diff --git a/.postman/ewm-main-service.json b/.postman/ewm-main-service.json new file mode 100644 index 0000000..62b600e --- /dev/null +++ b/.postman/ewm-main-service.json @@ -0,0 +1,14630 @@ +{ + "info": { + "_postman_id": "4f622f31-328a-4506-95bd-66359cfbe749", + "name": "Test Explore With Me - Main service", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "23073145", + "_collection_link": "https://universal-shadow-295426.postman.co/workspace/My-Workspace~4200f6aa-0504-44b1-8a1d-707d0dcbd5ce/collection/13708500-4f622f31-328a-4506-95bd-66359cfbe749?action=share&source=collection_link&creator=23073145" + }, + "item": [ + { + "name": "Validation", + "item": [ + { + "name": "Event", + "item": [ + { + "name": "Required query params", + "item": [ + { + "name": "Добавление запроса от текущего пользователя на участие в событии без обязательного query params", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " pm.request.removeQueryParams(['eventId']);\r", + " pm.collectionVariables.set('uid', submittedUser.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/requests", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "requests" + ], + "query": [ + { + "key": "eventId", + "value": "0", + "description": "(Required) id события", + "disabled": true + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- нельзя добавить повторный запрос\n- инициатор события не может добавить запрос на участие в своём событии\n- нельзя участвовать в неопубликованном событии\n- если у события достигнут лимит запросов на участие - необходимо вернуть ошибку\n- если для события отключена пре-модерация запросов на участие, то запрос должен автоматически перейти в состояние подтвержденного" + }, + "response": [] + } + ] + }, + { + "name": "Unrequired query params", + "item": [ + { + "name": "Получение событий, добавленных текущим пользователем без нескольких Query params", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "query": [ + { + "key": "from", + "value": "0", + "description": "количество элементов, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "1000", + "description": "количество элементов в наборе", + "disabled": true + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + } + }, + "response": [] + }, + { + "name": "Получение событий с возможностью фильтрации без нескольких Query params", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " pm.request.removeQueryParams(['text', 'categories', 'paid']);\r", + " pm.request.addQueryParams([`text=` + event.annotation, 'categories=' + category.id, 'paid=' + event.paid]);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "events" + ], + "query": [ + { + "key": "text", + "value": "0", + "description": "текст для поиска в содержимом аннотации и подробном описании события", + "disabled": true + }, + { + "key": "categories", + "value": "0", + "description": "список идентификаторов категорий в которых будет вестись поиск", + "disabled": true + }, + { + "key": "paid", + "value": "true", + "description": "поиск только платных/бесплатных событий", + "disabled": true + }, + { + "key": "rangeStart", + "value": "2022-01-06%2013%3A30%3A38", + "description": "дата и время не раньше которых должно произойти событие", + "disabled": true + }, + { + "key": "rangeEnd", + "value": "2097-09-06%2013%3A30%3A38", + "description": "дата и время не позже которых должно произойти событие", + "disabled": true + }, + { + "key": "onlyAvailable", + "value": "false", + "description": "только события у которых не исчерпан лимит запросов на участие", + "disabled": true + }, + { + "key": "sort", + "value": "EVENT_DATE", + "description": "Вариант сортировки: по дате события или по количеству просмотров", + "disabled": true + }, + { + "key": "from", + "value": "0", + "description": "количество событий, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "1000", + "description": "количество событий в наборе", + "disabled": true + } + ] + }, + "description": "Обратите внимание: \n- это публичный эндпоинт, соответственно в выдаче должны быть только опубликованные события\n- текстовый поиск (по аннотации и подробному описанию) должен быть без учета регистра букв\n- если в запросе не указан диапазон дат [rangeStart-rangeEnd], то нужно выгружать события, которые произойдут позже текущей даты и времени\n- информация о каждом событии должна включать в себя количество просмотров и количество уже одобренных заявок на участие\n- информацию о том, что по этому эндпоинту был осуществлен и обработан запрос, нужно сохранить в сервисе статистики" + }, + "response": [] + } + ] + }, + { + "name": "Required params in body", + "item": [ + { + "name": "Добавление события без поля description", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " delete event[\"description\"];\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление события с пустым описанием", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event[\"description\"] = '';\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление события со строкой из пробелов в качестве описания", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event[\"description\"] = ' ';\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление события без поля annotation", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " delete event[\"annotation\"];\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление события с пустой аннотацией", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event[\"annotation\"] = '';\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление события со строкой из пробелов в качестве аннотации", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event[\"annotation\"] = ' ';\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление события с отрицательным лимитом участников", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event[\"participantLimit\"] = -6;\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Изменение события добавленного текущим пользователем. Изменение лимита участников на отрицательное значение", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.collectionVariables.set(\"response\", event);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " participantLimit : -156\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.have.status(400);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + } + ] + }, + { + "name": "Misc tests", + "item": [ + { + "name": "Отклонение публикации события", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let new_event, event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " await pm.sendRequest({\r", + " url : \"http://localhost:8080/admin/events/\" + event.id,\r", + " method : \"PATCH\",\r", + " header: { \"Content-Type\": \"application/json\" },\r", + " body: JSON.stringify({\r", + " stateAction: \"REJECT_EVENT\"\r", + " })\r", + " }, (error, response) => {\r", + "\r", + " });\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.collectionVariables.set(\"response\", event);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " stateAction: \"SEND_TO_REVIEW\"\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get(\"response\");\r", + "const target = pm.response.json();\r", + "\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, description, participantLimit, state, createdOn, location, requestModeration\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('annotation');\r", + "pm.expect(target).to.have.property('category');\r", + "pm.expect(target).to.have.property('paid');\r", + "pm.expect(target).to.have.property('eventDate');\r", + "pm.expect(target).to.have.property('initiator');\r", + "pm.expect(target).to.have.property('description');\r", + "pm.expect(target).to.have.property('participantLimit');\r", + "pm.expect(target).to.have.property('state');\r", + "pm.expect(target).to.have.property('createdOn');\r", + "pm.expect(target).to.have.property('location');\r", + "pm.expect(target).to.have.property('requestModeration');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.annotation).equal(target.annotation, 'Аннотация отменённого события должна соответствовать аннотации события до отмены');\r", + " pm.expect(source.category.id).equal(target.category.id, 'Категория отменённого события должна соответствовать категории события до отмены');\r", + " pm.expect(source.paid.toString()).equal(target.paid.toString(), 'Стоимость отменённого события должна соответствовать стоимости события до отмены');\r", + " pm.expect(source.eventDate).equal(target.eventDate, 'Дата проведения отменённого события должна соответствовать дате проведения события до отмены');\r", + " pm.expect(source.description).equal(target.description, 'Описание отменённого события должно соответствовать описанию события до отмены');\r", + " pm.expect(source.title).equal(target.title, 'Название отменённого события должно соответствовать названию события до отмены');\r", + " pm.expect(source.participantLimit.toString()).equal(target.participantLimit.toString(), 'Лимит участников отменённого события должен соответствовать лимиту участников события до отмены');\r", + " pm.expect(source.paid.toString()).equal(target.paid.toString(), 'Стоимость отменённого события должна соответствовать стоимости события до отмены');\r", + "});\r", + "\r", + "pm.test(\"Событие должно иметь статус CANCELED при возвращении от администратора и статус PENDING после выполнения запроса\", function () {\r", + " pm.expect(target.state).equal(\"PENDING\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Попытка получения информации о событии по публичному эндпоинту без публикации", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.collectionVariables.set('response', event);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 404 и данные в формате json\", function () {\r", + " pm.response.to.be.notFound; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/events/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "events", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n- событие должно быть опубликовано\n- информация о событии должна включать в себя количество просмотров и количество подтвержденных запросов\n- информацию о том, что по этому эндпоинту был осуществлен и обработан запрос, нужно сохранить в сервисе статистики" + }, + "response": [] + }, + { + "name": "Поиск событий с проверкой параметров", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const user2 = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = rnd.getEvent(category.id)\r", + " event.requestModeration = true;\r", + " event.participantLimit = 2;\r", + " event = await api.addEvent(user.id, event);\r", + " event = await api.publishEvent(event.id);\r", + " pm.request.removeQueryParams(['users', 'categories']);\r", + " pm.request.addQueryParams([`users=` + user.id, 'categories=' + category.id]);\r", + " pm.collectionVariables.set('response', event);\r", + " await pm.sendRequest({\r", + " url : \"http://localhost:8080/admin/events?users=\" + user.id +\"&states=PUBLISHED&categories=\" + category.id + \"&rangeStart=2022-01-06%2013%3A30%3A38&rangeEnd=2097-09-06%2013%3A30%3A38&from=0&size=1000\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " pm.collectionVariables.set('confirmedRequests', response.json()[0].confirmedRequests)\r", + " });\r", + " const requestToJoin = await api.publishParticipationRequest(event.id, user2.id);\r", + " const confirmedRequest = await api.acceptParticipationRequest(event.id, user.id, requestToJoin.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json()[0];\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, views, confirmedRequests, description, participantLimit, state, createdOn, publishedOn, location, requestModeration\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('annotation');\r", + "pm.expect(target).to.have.property('category');\r", + "pm.expect(target).to.have.property('paid');\r", + "pm.expect(target).to.have.property('eventDate');\r", + "pm.expect(target).to.have.property('initiator');\r", + "pm.expect(target).to.have.property('views');\r", + "pm.expect(target).to.have.property('confirmedRequests');\r", + "pm.expect(target).to.have.property('description');\r", + "pm.expect(target).to.have.property('participantLimit');\r", + "pm.expect(target).to.have.property('state');\r", + "pm.expect(target).to.have.property('createdOn');\r", + "pm.expect(target).to.have.property('publishedOn');\r", + "pm.expect(target).to.have.property('location');\r", + "pm.expect(target).to.have.property('requestModeration');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.annotation).equal(target.annotation, 'Аннотация события должна соответствовать искомому событию');\r", + " pm.expect(source.category.id).equal(target.category.id, 'Идентификатор категории должен соответствовать искомой категории');\r", + " pm.expect(source.paid.toString()).equal(target.paid.toString(), 'Стоимость посещения события должна соответствовать искомому событию');\r", + " pm.expect(source.eventDate).equal(target.eventDate, 'Дата проведения события должна соответствовать дате искомого события');\r", + " pm.expect(source.description).equal(target.description, 'Описание события должно соответствовать искомому событию');\r", + " pm.expect(source.title).equal(target.title, 'Название события должно соответствовать искомому событию');\r", + " pm.expect(source.participantLimit.toString()).equal(target.participantLimit.toString(), 'Число участников события должно соответствовать искомому событию');\r", + " pm.expect(pm.collectionVariables.get('confirmedRequests')).equal(0);\r", + " pm.expect(target.confirmedRequests).equal(1);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/events?users=0&states=PUBLISHED&categories=0&rangeStart=2022-01-06%2013%3A30%3A38&rangeEnd=2097-09-06%2013%3A30%3A38&from=0&size=1000", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events" + ], + "query": [ + { + "key": "users", + "value": "0", + "description": "список id пользователей, чьи события нужно найти" + }, + { + "key": "states", + "value": "PUBLISHED", + "description": "список состояний в которых находятся искомые события" + }, + { + "key": "categories", + "value": "0", + "description": "список id категорий в которых будет вестись поиск" + }, + { + "key": "rangeStart", + "value": "2022-01-06%2013%3A30%3A38", + "description": "дата и время не раньше которых должно произойти событие" + }, + { + "key": "rangeEnd", + "value": "2097-09-06%2013%3A30%3A38", + "description": "дата и время не позже которых должно произойти событие" + }, + { + "key": "from", + "value": "0", + "description": "количество событий, которые нужно пропустить для формирования текущего набора" + }, + { + "key": "size", + "value": "1000", + "description": "количество событий в наборе" + } + ] + }, + "description": "Эндпоинт возвращает полную информацию обо всех событиях подходящих под переданные условия" + }, + "response": [] + }, + { + "name": "Получение событий с возможностью фильтрации и проверкой на валидацию", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " \r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/events?text=0&categories=0&paid=true&rangeStart=2022-01-06%2013%3A30%3A38&rangeEnd=2007-09-06%2013%3A30%3A38&onlyAvailable=false&sort=EVENT_DATE&from=0&size=1000", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "events" + ], + "query": [ + { + "key": "text", + "value": "0", + "description": "текст для поиска в содержимом аннотации и подробном описании события" + }, + { + "key": "categories", + "value": "0", + "description": "список идентификаторов категорий в которых будет вестись поиск" + }, + { + "key": "paid", + "value": "true", + "description": "поиск только платных/бесплатных событий" + }, + { + "key": "rangeStart", + "value": "2022-01-06%2013%3A30%3A38", + "description": "дата и время не раньше которых должно произойти событие" + }, + { + "key": "rangeEnd", + "value": "2007-09-06%2013%3A30%3A38", + "description": "дата и время не позже которых должно произойти событие" + }, + { + "key": "onlyAvailable", + "value": "false", + "description": "только события у которых не исчерпан лимит запросов на участие" + }, + { + "key": "sort", + "value": "EVENT_DATE", + "description": "Вариант сортировки: по дате события или по количеству просмотров" + }, + { + "key": "from", + "value": "0", + "description": "количество событий, которые нужно пропустить для формирования текущего набора" + }, + { + "key": "size", + "value": "1000", + "description": "количество событий в наборе" + } + ] + }, + "description": "Обратите внимание: \n- это публичный эндпоинт, соответственно в выдаче должны быть только опубликованные события\n- текстовый поиск (по аннотации и подробному описанию) должен быть без учета регистра букв\n- если в запросе не указан диапазон дат [rangeStart-rangeEnd], то нужно выгружать события, которые произойдут позже текущей даты и времени\n- информация о каждом событии должна включать в себя количество просмотров и количество уже одобренных заявок на участие\n- информацию о том, что по этому эндпоинту был осуществлен и обработан запрос, нужно сохранить в сервисе статистики" + }, + "response": [] + }, + { + "name": "Проверка работы поля views", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " await api.findEvent(event.id)\r", + " await api.findEvent(event.id)\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, views, confirmedRequests, description, participantLimit, state, createdOn, publishedOn, location, requestModeration\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('annotation');\r", + "pm.expect(target).to.have.property('category');\r", + "pm.expect(target).to.have.property('paid');\r", + "pm.expect(target).to.have.property('eventDate');\r", + "pm.expect(target).to.have.property('initiator');\r", + "pm.expect(target).to.have.property('views');\r", + "pm.expect(target).to.have.property('confirmedRequests');\r", + "pm.expect(target).to.have.property('description');\r", + "pm.expect(target).to.have.property('participantLimit');\r", + "pm.expect(target).to.have.property('state');\r", + "pm.expect(target).to.have.property('createdOn');\r", + "pm.expect(target).to.have.property('publishedOn');\r", + "pm.expect(target).to.have.property('location');\r", + "pm.expect(target).to.have.property('requestModeration');\r", + "});\r", + "\r", + "pm.test(\"Значение поля views должно увеличится на 1 после выполнения GET запроса с уникального IP к событию\", function () {\r", + " pm.expect(target.views).equal(1);\r", + " \r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/events/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "events", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n- событие должно быть опубликовано\n- информация о событии должна включать в себя количество просмотров и количество подтвержденных запросов\n- информацию о том, что по этому эндпоинту был осуществлен и обработан запрос, нужно сохранить в сервисе статистики" + }, + "response": [] + }, + { + "name": "Добавление запроса на участие при participantLimit == 0", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " eventBody['participantLimit'] = 0\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " pm.request.removeQueryParams(['eventId']);\r", + " pm.request.addQueryParams([`eventId=` + event.id]);\r", + " pm.collectionVariables.set('uid', submittedUser.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201); \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "var query = {};\r", + "pm.request.url.query.all().forEach((param) => { query[param.key] = param.value});\r", + "\r", + "pm.test(\"Запрос на участие должен содержать поля: id, requester, event, status, created\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('requester');\r", + "pm.expect(target).to.have.property('event');\r", + "pm.expect(target).to.have.property('status');\r", + "pm.expect(target).to.have.property('created');\r", + "});\r", + "\r", + "pm.test(\"При создании у запроса на участие должен быть статус CONFIRMED\", function () {\r", + " pm.expect(target.status).equal(\"CONFIRMED\");\r", + "});\r", + "\r", + "pm.test(\"Id ивента в запросе и в ответе должны совпадать\", function () {\r", + " pm.expect(target.event.toString()).equal(query['eventId'].toString());\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/requests?eventId=0", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "requests" + ], + "query": [ + { + "key": "eventId", + "value": "0", + "description": "(Required) id события" + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- нельзя добавить повторный запрос\n- инициатор события не может добавить запрос на участие в своём событии\n- нельзя участвовать в неопубликованном событии\n- если у события достигнут лимит запросов на участие - необходимо вернуть ошибку\n- если для события отключена пре-модерация запросов на участие, то запрос должен автоматически перейти в состояние подтвержденного" + }, + "response": [] + }, + { + "name": "Изменение даты события на уже наступившую", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " eventDate : \"2020-10-11 23:10:05\"\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.have.status(400);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Изменение события добавленного текущим пользователем. Изменение даты не неподходящую", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.collectionVariables.set(\"response\", event);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " eventDate : \"2020-10-11 23:10:05\"\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.have.status(400);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Добавление события на неподходящую дату", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event.eventDate = \"2020-12-31 15:10:05\";\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.have.status(400);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + } + ] + }, + { + "name": "String length restrictions", + "item": [ + { + "name": "Добавление нового события с description.length < 20", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event.description = rnd.getWord(19);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление нового события с description.length < 20", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event.description = rnd.getWord(19);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление нового события с annotation.length < 20", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event.annotation = rnd.getWord(19);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление нового события с annotation.length > 2000", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event.annotation = rnd.getWord(2001);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление нового события с description.length == 20 && annotation.length == 20 && title.length == 3", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event.description = rnd.getWord(20);\r", + " event.annotation = rnd.getWord(20);\r", + " event.title = rnd.getWord(3);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление нового события с description.length == 7000 && annotation.length == 2000 && title.length == 120", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event.description = rnd.getWord(7000);\r", + " event.annotation = rnd.getWord(2000);\r", + " event.title = rnd.getWord(120);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление нового события с title.length < 3", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event.title = rnd.getWord(2);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление нового события с title.length > 120", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event.title = rnd.getWord(121);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Изменение заголовка события с title.length < 3 (admin endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " let event2 = rnd.getEvent(category.id)\r", + " event2.title = rnd.getWord(2);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: event2,\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Изменение заголовка события с title.length > 120 (admin endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " let event2 = rnd.getEvent(category.id)\r", + " event2.title = rnd.getWord(121);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: event2,\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Изменение описания события с description.length < 20 (admin endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " let event2 = rnd.getEvent(category.id)\r", + " event2.description = rnd.getWord(19);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: event2,\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Изменение описания события с description.length > 7000 (admin endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " let event2 = rnd.getEvent(category.id)\r", + " event2.description = rnd.getWord(7001);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: event2,\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Изменение аннотации события с annotation.length < 20 (admin endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " let event2 = rnd.getEvent(category.id)\r", + " event2.annotation = rnd.getWord(19);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: event2,\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Изменение аннотации события с annotation.length > 2000 (admin endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " let event2 = rnd.getEvent(category.id)\r", + " event2.annotation = rnd.getWord(2001);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: event2,\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Изменение события с description.length == 20 && annotation.length == 20 && title.length == 3 (admin endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " let event2 = rnd.getEvent(category.id)\r", + " event2.annotation = rnd.getWord(20);\r", + " event2.description = rnd.getWord(20);\r", + " event2.title = rnd.getWord(3);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: event2,\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Изменение события с description.length == 7000 && annotation.length == 2000 && title.length == 120 (admin endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " let event2 = rnd.getEvent(category.id)\r", + " event2.annotation = rnd.getWord(2000);\r", + " event2.description = rnd.getWord(7000);\r", + " event2.title = rnd.getWord(120);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: event2,\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Изменение заголовка события с title.length < 3 (user endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " title: rnd.getWord(2)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Изменение заголовка события с title.length > 120 (user endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " title: rnd.getWord(121)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Изменение описания события с description.length < 20 (user endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " description: rnd.getWord(19)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Изменение описания события с description.length > 7000 (user endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " description: rnd.getWord(7001)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Изменение аннотации события с annotation.length < 20 (user endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " annotation: rnd.getWord(19)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Изменение аннотации события с annotation.length > 2000 (user endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " annotation: rnd.getWord(2001)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Изменение события с description.length == 20 && annotation.length == 20 && title.length == 3 (user endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " description: rnd.getWord(20),\r", + " annotation: rnd.getWord(20),\r", + " title: rnd.getWord(3)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Изменение события с description.length == 7000 && annotation.length == 2000 && title.length == 120 (user endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " description: rnd.getWord(7000),\r", + " annotation: rnd.getWord(2000),\r", + " title: rnd.getWord(120)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + } + ] + }, + { + "name": "Default values check", + "item": [ + { + "name": "Добавление нового события без paid, participantLimit, requestModeration", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let usersArr = Array.from({length: 10}, () => rnd.getUser());\r", + " let categoriesArr = Array.from({length: 10}, () => rnd.getCategory());\r", + " let usersResponseArr = [], categoriesResponseArr = [], eventArr, eventResponseArr = [];\r", + " try {\r", + " for (const u of usersArr){\r", + " usersResponseArr.push(await api.addUser(u));\r", + " }\r", + " for (const c of categoriesArr){\r", + " categoriesResponseArr.push(await api.addCategory(c));\r", + " }\r", + " eventArr = Array.from(categoriesResponseArr, (x) => rnd.getEvent(x.id));\r", + " for (let i = 0; i < 10; i++){\r", + " delete eventArr[i].requestModeration;\r", + " delete eventArr[i].paid;\r", + " delete eventArr[i].participantLimit;\r", + " }\r", + " for (let i = 0; i < 10; i++){\r", + " eventResponseArr.push(await api.addEvent(usersResponseArr[i].id, eventArr[i]));\r", + " }\r", + " pm.collectionVariables.set('responseArr', eventResponseArr)\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201); \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('responseArr');\r", + "\r", + "\r", + "pm.test(\"У каждого созданного события paid должно принять значение по умолчанию(false)\", function () {\r", + " source.forEach(function(x){pm.expect(x.paid).to.be.equal(false)});\r", + "});\r", + "\r", + "pm.test(\"У каждого созданного события participantLimit должен принять значение по умолчанию(0)\", function () {\r", + " source.forEach(function(x){pm.expect(x.participantLimit).to.be.equal(0)});\r", + "});\r", + "\r", + "pm.test(\"У каждого созданного события requestModeration должно принять значение по умолчанию(true)\", function () {\r", + " source.forEach(function(x){pm.expect(x.requestModeration).to.be.equal(true)});\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Проверка на значения по-умолчанию from и size(event)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user, category, eventArr;\r", + " try {\r", + " user = await api.addUser(rnd.getUser());\r", + " category = await api.addCategory(rnd.getCategory());\r", + " eventArr = Array.from({length:11}, () => rnd.getEvent(category.id));\r", + " for (let i = 0; i < 11; i++){\r", + " await api.addEvent(user.id, eventArr[i]);\r", + " }\r", + " await pm.sendRequest({\r", + " url : \"http://localhost:8080/admin/events?from=0\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {pm.collectionVariables.set('source', response.json())});\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "const source = pm.collectionVariables.get('source');\r", + "\r", + "pm.test(\"Значение from по-умолчанию должно быть равным 0\", function () {\r", + " pm.expect(target[0].id).to.be.equal(source[0].id, 'Запросы с from=0 и без него должны начинаться с одного и того же события');\r", + "});\r", + "\r", + "pm.test(\"Значение size по-умолчанию должно быть равным 10\", function () {\r", + " pm.expect(target.length).to.be.equal(10);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events" + ], + "query": [ + { + "key": "users", + "value": "0", + "description": "список id пользователей, чьи события нужно найти", + "disabled": true + }, + { + "key": "states", + "value": "PUBLISHED", + "description": "список состояний в которых находятся искомые события", + "disabled": true + }, + { + "key": "categories", + "value": "0", + "description": "список id категорий в которых будет вестись поиск", + "disabled": true + }, + { + "key": "rangeStart", + "value": "2022-01-06%2013%3A30%3A38", + "description": "дата и время не раньше которых должно произойти событие", + "disabled": true + }, + { + "key": "rangeEnd", + "value": "2097-09-06%2013%3A30%3A38", + "description": "дата и время не позже которых должно произойти событие", + "disabled": true + }, + { + "key": "from", + "value": "0", + "description": "количество событий, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "1000", + "description": "количество событий в наборе", + "disabled": true + } + ] + }, + "description": "Эндпоинт возвращает полную информацию обо всех событиях подходящих под переданные условия" + }, + "response": [] + }, + { + "name": "Получение событий, добавленных текущим пользователем. Проверка на значения по-умолчанию size и from", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let eventArr, user, category, eventResponseArr = [];\r", + " try {\r", + " user = await api.addUser(rnd.getUser());\r", + " category = await api.addCategory(rnd.getCategory());\r", + " eventArr = Array.from({length:11}, () => rnd.getEvent(category.id));\r", + " for (let i = 0; i < 11; i++){\r", + " eventResponseArr.push(await api.addEvent(user.id, eventArr[i]));\r", + " }\r", + " pm.collectionVariables.set('responseArr', eventResponseArr)\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " await pm.sendRequest({\r", + " url : \"http://localhost:8080/users/\" + user.id + \"/events?from=0\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {pm.collectionVariables.set('source', response.json())});\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "const source = pm.collectionVariables.get('responseArr');\r", + "newSourceArr = Array.from(source, (x) => x.id);\r", + "const responseWithFrom = pm.collectionVariables.get('source');\r", + "\r", + "pm.test(\"Значение size по-умолчанию должно быть равным 10\", function () {\r", + " pm.expect(target.length).to.equal(10);\r", + "});\r", + "\r", + "pm.test(\"Значение from по-умолчанию должно быть равным 0\", function () {\r", + " pm.expect(target[0].id).to.be.equal(responseWithFrom[0].id, 'Запросы с from=0 и без него должны начинаться с одного и того же события');\r", + "});\r", + "\r", + "pm.test(\"Все найденные события должны быть в списке добавленных\", function () {\r", + " source.forEach(function(x){pm.expect(newSourceArr).to.include(x.id)});\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "query": [ + { + "key": "from", + "value": "0", + "description": "количество элементов, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "1000", + "description": "количество элементов в наборе", + "disabled": true + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + } + }, + "response": [] + }, + { + "name": "Получение событий с возможностью фильтрации. Проверка на значение по-умолчанию size", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user, category, eventArr, eventResponseArr = [], publishEventResponseArr = [];\r", + " try {\r", + " user = await api.addUser(rnd.getUser());\r", + " category = await api.addCategory(rnd.getCategory());\r", + " eventArr = Array.from({length:11}, () => rnd.getEvent(category.id));\r", + " for (let i = 0; i < 11; i++){\r", + " eventResponseArr.push(await api.addEvent(user.id, eventArr[i]));\r", + " }\r", + " for (let i = 0; i < 11; i++){\r", + " publishEventResponseArr.push(await api.publishEvent(eventResponseArr[i].id));\r", + " }\r", + " pm.collectionVariables.set('responseArr', eventResponseArr);\r", + " pm.collectionVariables.set('catid', category.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "const source = pm.collectionVariables.get('responseArr');\r", + "newSourceArr = Array.from(source, (x) => x.id);\r", + "\r", + "pm.test(\"Значение size по-умолчанию должно быть равным 10\", function () {\r", + " pm.expect(target.length).to.equal(10);\r", + "});\r", + "\r", + "pm.test(\"Все найденные события должны быть в списке добавленных\", function () {\r", + " source.forEach(function(x){pm.expect(newSourceArr).to.include(x.id)});\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/events?categories={{catid}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "events" + ], + "query": [ + { + "key": "text", + "value": "0", + "description": "текст для поиска в содержимом аннотации и подробном описании события", + "disabled": true + }, + { + "key": "categories", + "value": "{{catid}}", + "description": "список идентификаторов категорий в которых будет вестись поиск" + }, + { + "key": "paid", + "value": "true", + "description": "поиск только платных/бесплатных событий", + "disabled": true + }, + { + "key": "rangeStart", + "value": "2022-01-06%2013%3A30%3A38", + "description": "дата и время не раньше которых должно произойти событие", + "disabled": true + }, + { + "key": "rangeEnd", + "value": "2097-09-06%2013%3A30%3A38", + "description": "дата и время не позже которых должно произойти событие", + "disabled": true + }, + { + "key": "onlyAvailable", + "value": "false", + "description": "только события у которых не исчерпан лимит запросов на участие", + "disabled": true + }, + { + "key": "sort", + "value": "EVENT_DATE", + "description": "Вариант сортировки: по дате события или по количеству просмотров", + "disabled": true + }, + { + "key": "from", + "value": "0", + "description": "количество событий, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "1000", + "description": "количество событий в наборе", + "disabled": true + } + ] + }, + "description": "Обратите внимание: \n- это публичный эндпоинт, соответственно в выдаче должны быть только опубликованные события\n- текстовый поиск (по аннотации и подробному описанию) должен быть без учета регистра букв\n- если в запросе не указан диапазон дат [rangeStart-rangeEnd], то нужно выгружать события, которые произойдут позже текущей даты и времени\n- информация о каждом событии должна включать в себя количество просмотров и количество уже одобренных заявок на участие\n- информацию о том, что по этому эндпоинту был осуществлен и обработан запрос, нужно сохранить в сервисе статистики" + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Users", + "item": [ + { + "name": "Required query params", + "item": [] + }, + { + "name": "Unrequired query params", + "item": [ + { + "name": "Поиск пользователей без нескольких Query params", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/users?ids={{uid}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ], + "query": [ + { + "key": "ids", + "value": "{{uid}}", + "description": "id пользователей" + }, + { + "key": "from", + "value": "0", + "description": "количество элементов, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "10", + "description": "количество элементов в наборе", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Поиск пользователей без параметра ids", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + " let user1 = rnd.getUser();\r", + " user1 = await api.addUser(user1);\r", + "\r", + " let user2 = rnd.getUser()\r", + " user2 = await api.addUser(user2);\r", + " //pm.collectionVariables.set('fromId', user1.id);\r", + " pm.collectionVariables.set('source1', user1);\r", + " pm.collectionVariables.set('source2', user2);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "const source1 = pm.collectionVariables.get('source1');\r", + "const source2 = pm.collectionVariables.get('source2');\r", + "\r", + "pm.test(\"Пользователи должны содержать поля: id, name, email\", function () {\r", + " pm.expect(target[target.length-2]).to.have.property('id');\r", + " pm.expect(target[target.length-2]).to.have.property('name');\r", + " pm.expect(target[target.length-2]).to.have.property('email');\r", + " pm.expect(target[target.length-1]).to.have.property('id');\r", + " pm.expect(target[target.length-1]).to.have.property('name');\r", + " pm.expect(target[target.length-1]).to.have.property('email');\r", + "});\r", + "\r", + "pm.test(\"Данные последних двух пользователей должны совпадать с данными добавленных пользователей\", function () {\r", + " pm.expect(target[target.length-2].id).to.equal(source1.id);\r", + " pm.expect(target[target.length-2].name).to.equal(source1.name);\r", + " pm.expect(target[target.length-2].email).to.equal(source1.email);\r", + " pm.expect(target[target.length-1].id).to.equal(source2.id);\r", + " pm.expect(target[target.length-1].name).to.equal(source2.name);\r", + " pm.expect(target[target.length-1].email).to.equal(source2.email);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/users?from={{fromId}}&size=100000", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ], + "query": [ + { + "key": "ids", + "value": "{{uid}}", + "description": "id пользователей", + "disabled": true + }, + { + "key": "from", + "value": "{{fromId}}", + "description": "количество элементов, которые нужно пропустить для формирования текущего набора" + }, + { + "key": "size", + "value": "100000", + "description": "количество элементов в наборе" + } + ] + } + }, + "response": [] + }, + { + "name": "Поиск событий без нескольких Query params", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " pm.request.removeQueryParams(['users', 'categories']);\r", + " pm.request.addQueryParams([`users=` + user.id, 'categories=' + category.id]);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events" + ], + "query": [ + { + "key": "users", + "value": "0", + "description": "список id пользователей, чьи события нужно найти", + "disabled": true + }, + { + "key": "states", + "value": "PUBLISHED", + "description": "список состояний в которых находятся искомые события", + "disabled": true + }, + { + "key": "categories", + "value": "0", + "description": "список id категорий в которых будет вестись поиск", + "disabled": true + }, + { + "key": "rangeStart", + "value": "2022-01-06%2013%3A30%3A38", + "description": "дата и время не раньше которых должно произойти событие", + "disabled": true + }, + { + "key": "rangeEnd", + "value": "2097-09-06%2013%3A30%3A38", + "description": "дата и время не позже которых должно произойти событие", + "disabled": true + }, + { + "key": "from", + "value": "0", + "description": "количество событий, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "1000", + "description": "количество событий в наборе", + "disabled": true + } + ] + }, + "description": "Эндпоинт возвращает полную информацию обо всех событиях подходящих под переданные условия" + }, + "response": [] + } + ] + }, + { + "name": "Required params in body", + "item": [ + { + "name": "Добавление пользователя с электронной почтой, состоящей только из пробелов", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.email = \" \";\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400\", function () {\r", + " pm.response.to.have.status(400);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с пустой электронной почтой", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.email = \"\";\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400\", function () {\r", + " pm.response.to.have.status(400);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя без поля email", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " delete user.email;\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400\", function () {\r", + " pm.response.to.have.status(400);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с пустым именем", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.name = \"\";\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400\", function () {\r", + " pm.response.to.have.status(400);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с именем, состоящим только из пробелов", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.name = \" \";\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400\", function () {\r", + " pm.response.to.have.status(400);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя без поля name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " delete user.name;\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400\", function () {\r", + " pm.response.to.have.status(400);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Misc tests", + "item": [] + }, + { + "name": "String length restrictions", + "item": [ + { + "name": "Добавление пользователя с name.length < 2", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.name = rnd.getWord(1);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с name.length == 2", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.name = rnd.getWord(2);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с name.length > 250", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.name = rnd.getWord(251);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с name.length == 250", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.name = rnd.getWord(250);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с email.length < 6", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.email = rnd.getWord(1) + '@a.r';\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с email.length == 6", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.email = rnd.getWord(1) + '@a.ru';\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с email.localpart.length > 64", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.email = rnd.getWord(65) + '@a.ru';\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с email.localpart.length == 64", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.email = rnd.getWord(59) + '@a.ru';\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с domain.part.length > 63", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.email = rnd.getWord(1) + '@' + rnd.getWord(64) + '.ru';\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с domain.part.length == 63", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.email = rnd.getWord(1) + '@' + rnd.getWord(60) + '.ru';\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с email.length > 254", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.email = rnd.getWord(1) + '@' + rnd.getWord(63) + '.' + rnd.getWord(63) + '.' + rnd.getWord(63) + '.' + rnd.getWord(61);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с email.length == 254", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.email = rnd.getWord(1) + '@' + rnd.getWord(63) + '.' + rnd.getWord(63) + '.' + rnd.getWord(63) + '.' + rnd.getWord(60);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Default values check", + "item": [ + { + "name": "Проверка на значения по-умолчанию from и size(user)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " for (let i = 0; i < 11; i++){\r", + " await api.addUser(rnd.getUser());\r", + " }\r", + " await pm.sendRequest({\r", + " url : \"http://localhost:8080/admin/users?from=0\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {pm.collectionVariables.set('source', response.json())});\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "const source = pm.collectionVariables.get('source');\r", + "\r", + "pm.test(\"Значение from по-умолчанию должно быть равным 0\", function () {\r", + " pm.expect(target[0].id).to.be.equal(source[0].id, 'Запросы с from=0 и без него должны начинаться с одного и того же события');\r", + "});\r", + "\r", + "pm.test(\"Значение size по-умолчанию должно быть равным 10\", function () {\r", + " pm.expect(target.length).to.be.equal(10);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ], + "query": [ + { + "key": "ids", + "value": "{{uid}}", + "description": "id пользователей", + "disabled": true + }, + { + "key": "ids", + "value": "-10833646", + "description": "id пользователей", + "disabled": true + }, + { + "key": "from", + "value": "0", + "description": "количество элементов, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "10", + "description": "количество элементов в наборе", + "disabled": true + } + ] + } + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Category", + "item": [ + { + "name": "Required query params", + "item": [] + }, + { + "name": "Unrequired params in body", + "item": [ + { + "name": "Получение категорий без нескольких Query params", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/categories", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "categories" + ], + "query": [ + { + "key": "from", + "value": "0", + "description": "количество категорий, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "10000", + "description": "количество категорий в наборе", + "disabled": true + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Required params in body", + "item": [ + { + "name": "Добавление категории с именем, состоящим из пробелов", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let category;\r", + " try {\r", + " category = {name: ' '};\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(category),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{baseUrl}}/admin/categories", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories" + ] + } + }, + "response": [] + }, + { + "name": "Добавление категории с пустым полем name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let category;\r", + " try {\r", + " category = {name: ''};\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(category),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{baseUrl}}/admin/categories", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories" + ] + } + }, + "response": [] + }, + { + "name": "Добавление категории без поля name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let category;\r", + " try {\r", + " category = {};\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(category),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{baseUrl}}/admin/categories", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Misc tests", + "item": [ + { + "name": "Изменение категории с неизменными данными", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + " let category, categoryObj\r", + " try {\r", + " category = rnd.getCategory();\r", + " categoryObj = await api.addCategory(category);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.collectionVariables.set(\"catid\", Number(categoryObj.id))\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(category),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Категория должна содержать поля: id, name\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('name');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + " pm.expect(source.name).equal(target.name, 'Название категории должно совпадать с отправленным');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/categories/:catId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories", + ":catId" + ], + "variable": [ + { + "key": "catId", + "value": "{{catid}}" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "String length restrictions", + "item": [ + { + "name": "Добавление новой категории с name.length > 50", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let category;\r", + " try {\r", + " category = {'name': rnd.getWord(51)};\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(category),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/categories", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories" + ] + }, + "description": "Обратите внимание: имя категории должно быть уникальным" + }, + "response": [] + }, + { + "name": "Добавление новой категории с name.length == 50", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let category;\r", + " try {\r", + " category = {'name': rnd.getWord(50)};\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(category),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/categories", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories" + ] + }, + "description": "Обратите внимание: имя категории должно быть уникальным" + }, + "response": [] + }, + { + "name": "Изменение имени категории с name.length > 50", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + " let category\r", + " try {\r", + " category = await api.addCategory(rnd.getCategory());\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.collectionVariables.set(\"catid\", Number(category.id))\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " name : rnd.getWord(51)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/categories/:catId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories", + ":catId" + ], + "variable": [ + { + "key": "catId", + "value": "{{catid}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Изменение имени категории с name.length == 50", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + " let category\r", + " try {\r", + " category = await api.addCategory(rnd.getCategory());\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.collectionVariables.set(\"catid\", Number(category.id))\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " name : rnd.getWord(50)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/categories/:catId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories", + ":catId" + ], + "variable": [ + { + "key": "catId", + "value": "{{catid}}" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Default values check", + "item": [ + { + "name": "Проверка на значения по-умолчанию from и size(category)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " for (let i = 0; i < 11; i++){\r", + " await api.addCategory(rnd.getCategory());\r", + " }\r", + " await pm.sendRequest({\r", + " url : \"http://localhost:8080/categories?from=0\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {pm.collectionVariables.set('source', response.json())});\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "const source = pm.collectionVariables.get('source');\r", + "\r", + "pm.test(\"Значение from по-умолчанию должно быть равным 0\", function () {\r", + " pm.expect(target[0].id).to.be.equal(source[0].id, 'Запросы с from=0 и без него должны начинаться с одного и того же события');\r", + "});\r", + "\r", + "pm.test(\"Значение size по-умолчанию должно быть равным 10\", function () {\r", + " pm.expect(target.length).to.be.equal(10);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/categories", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "categories" + ], + "query": [ + { + "key": "from", + "value": "0", + "description": "количество категорий, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "1000", + "description": "количество категорий в наборе", + "disabled": true + } + ] + } + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Compilation", + "item": [ + { + "name": "Required query params", + "item": [] + }, + { + "name": "Unreqired params in body", + "item": [ + { + "name": "Получение подборок событий без нескольких Query params", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const compilation = await api.addCompilation(rnd.getCompilation());\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "compilations" + ], + "query": [ + { + "key": "pinned", + "value": "true", + "description": "искать только закрепленные/не закрепленные подборки", + "disabled": true + }, + { + "key": "from", + "value": "0", + "description": "количество элементов, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "1000", + "description": "количество элементов в наборе", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Добавление новой подборки без параметра pinned", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " compilation = rnd.getCompilation(event.id);\r", + " delete compilation['pinned'];\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(compilation),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Подборка должны содержать поля: id, title, pinned, events\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('pinned');\r", + "pm.expect(target).to.have.property('events');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + " pm.expect(target.title).to.be.a(\"string\");\r", + " pm.expect(target.events).to.be.an(\"array\");\r", + " pm.expect(target.pinned).equal(false);\r", + "\r", + " pm.expect(source.events[0]).equal(target.events[0].id, 'Идентификаторы событий в подборке должен быть идентичен идентификаторам, указанным при создании подборки ');\r", + " pm.expect(source.title).equal(target.title, 'Название подборки должно соответствовать указанному при создании');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Required params in body", + "item": [ + { + "name": "Добавление подборки без поля title", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " compilation = {\r", + " \"pinned\":\"true\"};\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(compilation),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations" + ] + } + }, + "response": [] + }, + { + "name": "Добавление подборки с пустым полем title", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " compilation = {\r", + " \"pinned\":\"true\",\r", + " \"title\": \"\"};\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(compilation),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations" + ] + } + }, + "response": [] + }, + { + "name": "Добавление подборки с пустой строкой в качестве названия", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " compilation = {\r", + " \"pinned\":\"true\",\r", + " \"title\": \" \"};\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(compilation),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Misc tests", + "item": [ + { + "name": "Добавление подборки с проверкой связей многие-ко-многим", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " let compilation2;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " const event2 = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " compilation = rnd.getCompilation(event.id, event2.id);\r", + " compilation2 = rnd.getCompilation(event.id, event2.id);\r", + " await pm.sendRequest({\r", + " url : \"http://localhost:8080/admin/compilations\",\r", + " method : \"POST\",\r", + " header: { \"Content-Type\": \"application/json\" },\r", + " body: JSON.stringify({\r", + " events: compilation2.events,\r", + " title: compilation2.title\r", + " })\r", + " }, (error, response) => {\r", + "\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(compilation),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Подборка должны содержать поля: id, title, pinned, events\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('pinned');\r", + "pm.expect(target).to.have.property('events');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + " pm.expect(target.title).to.be.a(\"string\");\r", + " pm.expect(target.events).to.be.an(\"array\");\r", + " if (target.events[0].id < target.events[1].id){\r", + " pm.expect(source.events[0]).equal(target.events[0].id, 'Идентификаторы событий в подборке должен быть идентичен идентификаторам, указанным при создании подборки ');\r", + " } else {\r", + " pm.expect(source.events[0]).equal(target.events[1].id, 'Идентификаторы событий в подборке должен быть идентичен идентификаторам, указанным при создании подборки ');\r", + " }\r", + " pm.expect(source.title).equal(target.title, 'Название подборки должно соответствовать указанному при создании');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations" + ] + } + }, + "response": [] + }, + { + "name": "Добавление подборки без событий", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " compilation = rnd.getCompilation();\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(compilation),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Подборка должны содержать поля: id, title, pinned, events\", function () {\r", + " pm.expect(target).to.have.property('id');\r", + " pm.expect(target).to.have.property('title');\r", + " pm.expect(target).to.have.property('pinned');\r", + " pm.expect(target).to.have.property('events');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + " pm.expect(target.title).to.be.a(\"string\");\r", + " pm.expect(target.events).to.be.an(\"array\");\r", + "\r", + " pm.expect(source.title).equal(target.title, 'Название подборки должно соответствовать указанному при создании');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "String length restrictions", + "item": [ + { + "name": "Добавление подборки с title.length > 50", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({title: rnd.getWord(51)}),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations" + ] + } + }, + "response": [] + }, + { + "name": "Добавление подборки с title.length == 50", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({title: rnd.getWord(50)}),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations" + ] + } + }, + "response": [] + }, + { + "name": "Обновить названия подборки с title.length > 50", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const compilation = await api.addCompilation(rnd.getCompilation());\r", + " pm.collectionVariables.set('compid', compilation.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " title: rnd.getWord(51)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations/:compId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations", + ":compId" + ], + "variable": [ + { + "key": "compId", + "value": "{{compid}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Обновить названия подборки с title.length == 50", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const compilation = await api.addCompilation(rnd.getCompilation());\r", + " pm.collectionVariables.set('compid', compilation.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " title: rnd.getWord(50)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations/:compId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations", + ":compId" + ], + "variable": [ + { + "key": "compId", + "value": "{{compid}}" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Default values check", + "item": [ + { + "name": "Добавление подборки без pinned", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let cArr = Array.from({length: 10}, () => rnd.getCompilation());\r", + " let responseArr = [];\r", + " try {\r", + " cArr.forEach(function(x){ delete x.pinned });\r", + " for (const c of cArr){\r", + " responseArr.push(await api.addCompilation(c));\r", + " }\r", + " pm.collectionVariables.set('responseArr', responseArr);\r", + " compilation = rnd.getCompilation();\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(compilation),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('responseArr');\r", + "\r", + "\r", + "pm.test(\"У каждой из созданных подборок pinned должно принять значение по умолчанию(false)\", function () {\r", + " source.forEach(function(x){pm.expect(x.pinned).to.be.equal(false)});\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations" + ] + } + }, + "response": [] + }, + { + "name": "Проверка на значения по-умолчанию from и size(compilation)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " for (let i = 0; i < 11; i++){\r", + " await api.addCompilation(rnd.getCompilation());\r", + " }\r", + " await pm.sendRequest({\r", + " url : \"http://localhost:8080/compilations?from=0\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {pm.collectionVariables.set('source', response.json())});\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "const source = pm.collectionVariables.get('source');\r", + "\r", + "pm.test(\"Значение from по-умолчанию должно быть равным 0\", function () {\r", + " pm.expect(target[0].id).to.be.equal(source[0].id, 'Запросы с from=0 и без него должны начинаться с одного и того же события');\r", + "});\r", + "\r", + "pm.test(\"Значение size по-умолчанию должно быть равным 10\", function () {\r", + " pm.expect(target.length).to.be.equal(10);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "compilations" + ], + "query": [ + { + "key": "pinned", + "value": "true", + "description": "искать только закрепленные/не закрепленные подборки", + "disabled": true + }, + { + "key": "from", + "value": "0", + "description": "количество элементов, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "1000", + "description": "количество элементов в наборе", + "disabled": true + } + ] + } + }, + "response": [] + } + ] + } + ] + } + ] + }, + { + "name": "409 Conflict", + "item": [ + { + "name": "Попытка изменения имени категории на уже существующее", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + " let category1, category2\r", + " try {\r", + " category1 = await api.addCategory(rnd.getCategory());\r", + " category2 = await api.addCategory(rnd.getCategory());\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.collectionVariables.set(\"catid\", category2.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " name : category1.name\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/categories/:catId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories", + ":catId" + ], + "variable": [ + { + "key": "catId", + "value": "{{catid}}" + } + ] + }, + "description": "Обратите внимание: имя категории должно быть уникальным" + }, + "response": [] + }, + { + "name": "Добавление новой категории с занятым именем", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let category;\r", + " try {\r", + " category = rnd.getCategory();\r", + " await api.addCategory(category);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(category),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/categories", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories" + ] + }, + "description": "Обратите внимание: имя категории должно быть уникальным" + }, + "response": [] + }, + { + "name": "Добавление пользователя с занятым именем почты", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.name = rnd.getWord(10);\r", + " await api.addUser(user);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Удаление категории с привязанными событиями", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const user = await api.addUser(rnd.getUser());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set('catid', category.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/categories/:catId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories", + ":catId" + ], + "variable": [ + { + "key": "catId", + "value": "{{catid}}" + } + ] + }, + "description": "Обратите внимание: с категорий не должно быть связано ни одного события." + }, + "response": [] + }, + { + "name": "Изменение имени категории на уже занятое", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + " let category1, category2\r", + " try {\r", + " category1 = await api.addCategory(rnd.getCategory());\r", + " category2 = await api.addCategory(rnd.getCategory());\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.collectionVariables.set(\"catid\", Number(category1.id))\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " name : category2.name\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/categories/:catId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories", + ":catId" + ], + "variable": [ + { + "key": "catId", + "value": "{{catid}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Публикация уже опубликованного события", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " stateAction : \"PUBLISH_EVENT\"\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Публикация отмененного события", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.rejectEvent(event.id);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " stateAction : \"PUBLISH_EVENT\"\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Отмена опубликованного события", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " stateAction : \"REJECT_EVENT\"\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Добавление повторного запроса от пользователя на участие в событии", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " await api.publishParticipationRequest(event.id, submittedUser.id);\r", + " pm.request.removeQueryParams(['eventId']);\r", + " pm.request.addQueryParams([`eventId=` + event.id]);\r", + " pm.collectionVariables.set('uid', submittedUser.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/requests?eventId=0", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "requests" + ], + "query": [ + { + "key": "eventId", + "value": "0", + "description": "(Required) id события" + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- нельзя добавить повторный запрос\n- инициатор события не может добавить запрос на участие в своём событии\n- нельзя участвовать в неопубликованном событии\n- если у события достигнут лимит запросов на участие - необходимо вернуть ошибку\n- если для события отключена пре-модерация запросов на участие, то запрос должен автоматически перейти в состояние подтвержденного" + }, + "response": [] + }, + { + "name": "Добавление запроса от инициатора события на участие в нём", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " pm.request.removeQueryParams(['eventId']);\r", + " pm.request.addQueryParams([`eventId=` + event.id]);\r", + " pm.collectionVariables.set('uid', user.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/requests?eventId=0", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "requests" + ], + "query": [ + { + "key": "eventId", + "value": "0", + "description": "(Required) id события" + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- нельзя добавить повторный запрос\n- инициатор события не может добавить запрос на участие в своём событии\n- нельзя участвовать в неопубликованном событии\n- если у события достигнут лимит запросов на участие - необходимо вернуть ошибку\n- если для события отключена пре-модерация запросов на участие, то запрос должен автоматически перейти в состояние подтвержденного" + }, + "response": [] + }, + { + "name": "Добавление запроса на участие в неопубликованном событии", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " pm.request.removeQueryParams(['eventId']);\r", + " pm.request.addQueryParams([`eventId=` + event.id]);\r", + " pm.collectionVariables.set('uid', submittedUser.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/requests?eventId=0", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "requests" + ], + "query": [ + { + "key": "eventId", + "value": "0", + "description": "(Required) id события" + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- нельзя добавить повторный запрос\n- инициатор события не может добавить запрос на участие в своём событии\n- нельзя участвовать в неопубликованном событии\n- если у события достигнут лимит запросов на участие - необходимо вернуть ошибку\n- если для события отключена пре-модерация запросов на участие, то запрос должен автоматически перейти в состояние подтвержденного" + }, + "response": [] + }, + { + "name": "Добавление запроса на участие в событии, у которого заполнен лимит участников", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody.participantLimit = 1;\r", + " eventBody.requestModeration = false;\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser1 = await api.addUser(rnd.getUser());\r", + " const submittedUser2 = await api.addUser(rnd.getUser());\r", + " await api.publishParticipationRequest(event.id, submittedUser1.id);\r", + " pm.request.removeQueryParams(['eventId']);\r", + " pm.request.addQueryParams([`eventId=` + event.id]);\r", + " pm.collectionVariables.set('uid', submittedUser2.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/requests?eventId=0", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "requests" + ], + "query": [ + { + "key": "eventId", + "value": "0", + "description": "(Required) id события" + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- нельзя добавить повторный запрос\n- инициатор события не может добавить запрос на участие в своём событии\n- нельзя участвовать в неопубликованном событии\n- если у события достигнут лимит запросов на участие - необходимо вернуть ошибку\n- если для события отключена пре-модерация запросов на участие, то запрос должен автоматически перейти в состояние подтвержденного" + }, + "response": [] + }, + { + "name": "Изменение опубликованного события от имени пользователя", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.collectionVariables.set(\"response\", event);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " eventDate : rnd.getFutureDateTime(6)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Попытка принять заявку на участие в событии, когда лимит уже достигнут", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true;\r", + " eventBody['participantLimit'] = 1;\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser1 = await api.addUser(rnd.getUser());\r", + " const submittedUser2 = await api.addUser(rnd.getUser());\r", + " const requestToParticipate = await api.publishParticipationRequest(event.id, submittedUser1.id);\r", + " const requestToParticipate2 = await api.publishParticipationRequest(event.id, submittedUser2.id);\r", + " await api.acceptParticipationRequest(event.id, user.id, requestToParticipate.id);\r", + " pm.collectionVariables.set('uid', user.id);\r", + " pm.collectionVariables.set('eid', event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({requestIds: [requestToParticipate2.id],\r", + " status:\"CONFIRMED\"}),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/requests", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "requests" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- если для события лимит заявок равен 0 или отключена пре-модерация заявок, то подтверждение заявок не требуется\n- нельзя подтвердить заявку, если уже достигнут лимит по заявкам на данное событие\n- статус можно изменить только у заявок, находящихся в состоянии ожидания\n- если при подтверждении данной заявки, лимит заявок для события исчерпан, то все неподтверждённые заявки необходимо отклонить" + }, + "response": [] + }, + { + "name": "Попытка отменить уже принятую заявку на участие в событии", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true;\r", + " eventBody['participantLimit'] = 1;\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " const requestToParticipate = await api.publishParticipationRequest(event.id, submittedUser.id);\r", + " await api.acceptParticipationRequest(event.id, user.id, requestToParticipate.id);\r", + " pm.collectionVariables.set('uid', user.id);\r", + " pm.collectionVariables.set('eid', event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({requestIds: [requestToParticipate.id],\r", + " status:\"REJECTED\"}),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/requests", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "requests" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- если для события лимит заявок равен 0 или отключена пре-модерация заявок, то подтверждение заявок не требуется\n- нельзя подтвердить заявку, если уже достигнут лимит по заявкам на данное событие\n- статус можно изменить только у заявок, находящихся в состоянии ожидания\n- если при подтверждении данной заявки, лимит заявок для события исчерпан, то все неподтверждённые заявки необходимо отклонить" + }, + "response": [] + } + ] + }, + { + "name": "Category", + "item": [ + { + "name": "Добавление новой категории", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let category;\r", + " try {\r", + " category = rnd.getCategory();\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(category),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Категория должна содержать поля: id, name\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('name');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + " pm.expect(source.name).equal(target.name, 'Название категории должно совпадать с отправленным');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/categories", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories" + ] + }, + "description": "Обратите внимание: имя категории должно быть уникальным" + }, + "response": [] + }, + { + "name": "Получение категорий", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " pm.collectionVariables.set(\"response\", category)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json();\r", + "let founded;\r", + "target.forEach(function(element){if (element.id == source.id) founded = element});\r", + "\r", + "pm.test(\"Категория должна содержать поля: id, name\", function () {\r", + "pm.expect(target[0]).to.have.property('id');\r", + "pm.expect(target[0]).to.have.property('name');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.id).equal(founded.id, 'Идентификатор категории должен соответствовать идентификатору категории добавленной ранее');\r", + " pm.expect(source.name).equal(founded.name, 'Название категории должно соответствовать названию категории добавленной ранее');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/categories?from=0&size=1000", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "categories" + ], + "query": [ + { + "key": "from", + "value": "0", + "description": "количество категорий, которые нужно пропустить для формирования текущего набора" + }, + { + "key": "size", + "value": "1000", + "description": "количество категорий в наборе" + } + ] + } + }, + "response": [] + }, + { + "name": "Получение информации о категории по её идентификатору", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " pm.collectionVariables.set(\"response\", category)\r", + " pm.collectionVariables.set(\"catid\", category.id)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Категория должна содержать поля: id, name\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('name');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.id).equal(target.id, 'Идентификатор категории должен соответствовать идентификатору в запросе');\r", + " pm.expect(source.name).equal(target.name, 'Название категории должно соответствовать названию категории с указанным идентификатором');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/categories/:catId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "categories", + ":catId" + ], + "variable": [ + { + "key": "catId", + "value": "{{catid}}", + "description": "(Required) id категории" + } + ] + } + }, + "response": [] + }, + { + "name": "Удаление категории", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const findedCategory = await api.findCategory(category.id);\r", + " pm.collectionVariables.set(\"catid\", category.id)\r", + " pm.collectionVariables.set(\"response\", findedCategory)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 204\", function () {\r", + " pm.response.to.have.status(204);\r", + "});\r", + "\r", + "source = pm.collectionVariables.get('response');\r", + "catId = pm.collectionVariables.get('catid');\r", + "\r", + "pm.test(\"Категория должна быть найдена до удаления\", function () {\r", + " pm.expect(source.id).equal(catId, 'Идентификтор категории должен совпадать с удаляемым');\r", + "});\r", + "\r", + "pm.sendRequest({\r", + " url: pm.collectionVariables.get(\"baseUrl\") + \"/categories/\" + catId,\r", + " method: 'GET',\r", + " }, (error, response) => {\r", + " pm.test(\"Категория не должна быть найдена после удаления\", function () {\r", + " pm.expect(response.code).to.eql(404);\r", + " });\r", + " });" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/categories/:catId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories", + ":catId" + ], + "variable": [ + { + "key": "catId", + "value": "{{catid}}" + } + ] + }, + "description": "Обратите внимание: с категорий не должно быть связано ни одного события." + }, + "response": [] + }, + { + "name": "Изменение категории", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + " let category\r", + " try {\r", + " category = await api.addCategory(rnd.getCategory());\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.collectionVariables.set(\"catid\", Number(category.id))\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " name : rnd.getCategory().name\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Категория должна содержать поля: id, name\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('name');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + " pm.expect(source.name).equal(target.name, 'Название категории должно совпадать с отправленным');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/categories/:catId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories", + ":catId" + ], + "variable": [ + { + "key": "catId", + "value": "{{catid}}" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Users", + "item": [ + { + "name": "Поиск пользователей", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Пользователи должны содержать поля: id, name, email\", function () {\r", + " pm.expect(target[0]).to.have.property('id');\r", + " pm.expect(target[0]).to.have.property('name');\r", + " pm.expect(target[0]).to.have.property('email');\r", + "});\r", + "\r", + "pm.test(\"Должен быть найден только один пользователь по заданному фильтру\", function () {\r", + " pm.expect(target.length).to.eql(1);\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target[0].id).equal(pm.collectionVariables.get(\"uid\"));\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/users?ids={{uid}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ], + "query": [ + { + "key": "ids", + "value": "{{uid}}", + "description": "id пользователей" + }, + { + "key": "ids", + "value": "-10833646", + "description": "id пользователей", + "disabled": true + }, + { + "key": "from", + "value": "0", + "description": "количество элементов, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "10", + "description": "количество элементов в наборе", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Добавление нового пользователя", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Пользователь должен содержать поля: id, name, email\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('name');\r", + "pm.expect(target).to.have.property('email');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + " pm.expect(source.name).equal(target.name, 'Имя пользователя должно соответствовать отправленному в запросе');\r", + " pm.expect(source.email).equal(target.email, 'Почта пользователя должна соответствовать отправленной в запросе');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Удаление пользователя", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 204\", function () {\r", + " pm.response.to.have.status(204);\r", + "});\r", + "const source = pm.collectionVariables.get('response');\r", + "const userId = pm.collectionVariables.get('uid');\r", + "\r", + "pm.test(\"Пользователь должен быть найден до выполнения запроса\", function(){\r", + " pm.expect(source.length).to.eql(1);\r", + " pm.expect(source[0].id).to.eql(userId);\r", + "});\r", + "let body\r", + "const req = {\r", + " url: \"http://localhost:8080/admin/users?ids=\" + pm.collectionVariables.get(\"uid\"),\r", + " method: \"GET\",\r", + " body: body == null ? \"\" : JSON.stringify(body),\r", + " header: { \"Content-Type\": \"application/json\" },\r", + " };\r", + "pm.sendRequest(req, (error, response) => {\r", + " pm.test(\"Пользователь должен быть удалён после выполнения запроса\", function(){\r", + " pm.expect(response.json().length).to.eql(0);\r", + " });\r", + "})" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const foundedUser = await api.findUser(user.id);\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"response\", foundedUser)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/users/:userId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users", + ":userId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id пользователя" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Event", + "item": [ + { + "name": "Добавление нового события", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201); \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, description, participantLimit, state, createdOn, location, requestModeration\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('annotation');\r", + "pm.expect(target).to.have.property('category');\r", + "pm.expect(target).to.have.property('paid');\r", + "pm.expect(target).to.have.property('eventDate');\r", + "pm.expect(target).to.have.property('initiator');\r", + "pm.expect(target).to.have.property('description');\r", + "pm.expect(target).to.have.property('participantLimit');\r", + "pm.expect(target).to.have.property('state');\r", + "pm.expect(target).to.have.property('createdOn');\r", + "pm.expect(target).to.have.property('location');\r", + "pm.expect(target).to.have.property('requestModeration');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + " pm.expect(target.title).equal(source.title, 'Название события должно соответствовать названию события в запросе');\r", + " pm.expect(target.annotation).equal(source.annotation, 'Аннотация события должна соответствовать аннотации события в запросе');\r", + " pm.expect(target.paid.toString()).equal(source.paid.toString(), 'Стоимость события должна соответствовать стоимости события в запросе');\r", + " pm.expect(target.eventDate).equal(source.eventDate, 'Дата проведения события должна соответствовать дате проведения события в запросе');\r", + " pm.expect(target.description).equal(source.description, 'Описание события должно соответствовать описание события в запросе');\r", + " pm.expect(target.participantLimit.toString()).equal(source.participantLimit.toString(), 'Лимит участников события должно соответствовать лимиту участников события в запросе');\r", + " pm.expect(target.location.lat.toString()).equal(source.location.lat.toString(), 'Широта локации проведения события должна соответствовать широте локации проведения события в запросе');\r", + " pm.expect(target.location.lon.toString()).equal(source.location.lon.toString(), 'Долгота локации проведения события должна соответствовать долготе локации проведения события в запросе');\r", + " pm.expect(target.requestModeration.toString()).equal(source.requestModeration.toString(), 'Необходимость модерации события должна соответствовать необходимости модерации события в запросе');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление запроса от текущего пользователя на участие в событии", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " pm.request.removeQueryParams(['eventId']);\r", + " pm.request.addQueryParams([`eventId=` + event.id]);\r", + " pm.collectionVariables.set('uid', submittedUser.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201); \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "var query = {};\r", + "pm.request.url.query.all().forEach((param) => { query[param.key] = param.value});\r", + "\r", + "pm.test(\"Запрос на участие должен содержать поля: id, requester, event, status, created\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('requester');\r", + "pm.expect(target).to.have.property('event');\r", + "pm.expect(target).to.have.property('status');\r", + "pm.expect(target).to.have.property('created');\r", + "});\r", + "\r", + "pm.test(\"При создании у запроса на участие должен быть статус PENDING\", function () {\r", + " pm.expect(target.status).equal(\"PENDING\");\r", + "});\r", + "\r", + "pm.test(\"Id ивента в запросе и в ответе должны совпадать\", function () {\r", + " pm.expect(target.event.toString()).equal(query['eventId'].toString());\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/requests?eventId=0", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "requests" + ], + "query": [ + { + "key": "eventId", + "value": "0", + "description": "(Required) id события" + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- нельзя добавить повторный запрос\n- инициатор события не может добавить запрос на участие в своём событии\n- нельзя участвовать в неопубликованном событии\n- если у события достигнут лимит запросов на участие - необходимо вернуть ошибку\n- если для события отключена пре-модерация запросов на участие, то запрос должен автоматически перейти в состояние подтвержденного" + }, + "response": [] + }, + { + "name": "Поиск событий", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " pm.request.removeQueryParams(['users', 'categories']);\r", + " pm.request.addQueryParams([`users=` + user.id, 'categories=' + category.id]);\r", + " pm.collectionVariables.set('response', event);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json()[0];\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, views, confirmedRequests, description, participantLimit, state, createdOn, publishedOn, location, requestModeration\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('annotation');\r", + "pm.expect(target).to.have.property('category');\r", + "pm.expect(target).to.have.property('paid');\r", + "pm.expect(target).to.have.property('eventDate');\r", + "pm.expect(target).to.have.property('initiator');\r", + "pm.expect(target).to.have.property('views');\r", + "pm.expect(target).to.have.property('confirmedRequests');\r", + "pm.expect(target).to.have.property('description');\r", + "pm.expect(target).to.have.property('participantLimit');\r", + "pm.expect(target).to.have.property('state');\r", + "pm.expect(target).to.have.property('createdOn');\r", + "pm.expect(target).to.have.property('publishedOn');\r", + "pm.expect(target).to.have.property('location');\r", + "pm.expect(target).to.have.property('requestModeration');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.annotation).equal(target.annotation, 'Аннотация события должна соответствовать искомому событию');\r", + " pm.expect(source.category.id).equal(target.category.id, 'Идентификатор категории должен соответствовать искомой категории');\r", + " pm.expect(source.paid.toString()).equal(target.paid.toString(), 'Стоимость посещения события должна соответствовать искомому событию');\r", + " pm.expect(source.eventDate).equal(target.eventDate, 'Дата проведения события должна соответствовать дате искомого события');\r", + " pm.expect(source.description).equal(target.description, 'Описание события должно соответствовать искомому событию');\r", + " pm.expect(source.title).equal(target.title, 'Название события должно соответствовать искомому событию');\r", + " pm.expect(source.participantLimit.toString()).equal(target.participantLimit.toString(), 'Число участников события должно соответствовать искомому событию');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/events?users=0&states=PUBLISHED&categories=0&rangeStart=2022-01-06%2013%3A30%3A38&rangeEnd=2097-09-06%2013%3A30%3A38&from=0&size=1000", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events" + ], + "query": [ + { + "key": "users", + "value": "0", + "description": "список id пользователей, чьи события нужно найти" + }, + { + "key": "states", + "value": "PUBLISHED", + "description": "список состояний в которых находятся искомые события" + }, + { + "key": "categories", + "value": "0", + "description": "список id категорий в которых будет вестись поиск" + }, + { + "key": "rangeStart", + "value": "2022-01-06%2013%3A30%3A38", + "description": "дата и время не раньше которых должно произойти событие" + }, + { + "key": "rangeEnd", + "value": "2097-09-06%2013%3A30%3A38", + "description": "дата и время не позже которых должно произойти событие" + }, + { + "key": "from", + "value": "0", + "description": "количество событий, которые нужно пропустить для формирования текущего набора" + }, + { + "key": "size", + "value": "1000", + "description": "количество событий в наборе" + } + ] + }, + "description": "Эндпоинт возвращает полную информацию обо всех событиях подходящих под переданные условия" + }, + "response": [] + }, + { + "name": "Получение событий, добавленных текущим пользователем", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json()[0];\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate\", function () {\r", + " pm.expect(target).to.contain.keys('id', 'title', 'annotation', 'category', 'paid', 'eventDate');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/events?from=0&size=1000", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "query": [ + { + "key": "from", + "value": "0", + "description": "количество элементов, которые нужно пропустить для формирования текущего набора" + }, + { + "key": "size", + "value": "1000", + "description": "количество элементов в наборе" + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + } + }, + "response": [] + }, + { + "name": "Получение событий с возможностью фильтрации", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " pm.request.removeQueryParams(['text', 'categories', 'paid']);\r", + " pm.request.addQueryParams([`text=` + event.annotation, 'categories=' + category.id, 'paid=' + event.paid]);\r", + " pm.collectionVariables.set('response', event);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json()[0];\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, views, confirmedRequests\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('annotation');\r", + "pm.expect(target).to.have.property('category');\r", + "pm.expect(target).to.have.property('paid');\r", + "pm.expect(target).to.have.property('eventDate');\r", + "pm.expect(target).to.have.property('initiator');\r", + "pm.expect(target).to.have.property('views');\r", + "pm.expect(target).to.have.property('confirmedRequests');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.annotation).equal(target.annotation, 'Аннотация события должна соответствовать аннотации события с указанным идентификатором');\r", + " pm.expect(source.category.id).equal(target.category.id, 'Категория события должна соответствовать категории события с указанным идентификатором');\r", + " pm.expect(source.paid.toString()).equal(target.paid.toString(), 'Стоимость события должна соответствовать стоимости события с указанным идентификатором');\r", + " pm.expect(source.eventDate).equal(target.eventDate, 'Дата проведения события должна соответствовать дате проведения события с указанным идентификатором');\r", + " pm.expect(source.title).equal(target.title, 'Название события должно соответствовать названию события с указанным идентификатором');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/events?text=0&categories=0&paid=true&rangeStart=2022-01-06%2013%3A30%3A38&rangeEnd=2097-09-06%2013%3A30%3A38&onlyAvailable=false&sort=EVENT_DATE&from=0&size=1000", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "events" + ], + "query": [ + { + "key": "text", + "value": "0", + "description": "текст для поиска в содержимом аннотации и подробном описании события" + }, + { + "key": "categories", + "value": "0", + "description": "список идентификаторов категорий в которых будет вестись поиск" + }, + { + "key": "paid", + "value": "true", + "description": "поиск только платных/бесплатных событий" + }, + { + "key": "rangeStart", + "value": "2022-01-06%2013%3A30%3A38", + "description": "дата и время не раньше которых должно произойти событие" + }, + { + "key": "rangeEnd", + "value": "2097-09-06%2013%3A30%3A38", + "description": "дата и время не позже которых должно произойти событие" + }, + { + "key": "onlyAvailable", + "value": "false", + "description": "только события у которых не исчерпан лимит запросов на участие" + }, + { + "key": "sort", + "value": "EVENT_DATE", + "description": "Вариант сортировки: по дате события или по количеству просмотров" + }, + { + "key": "from", + "value": "0", + "description": "количество событий, которые нужно пропустить для формирования текущего набора" + }, + { + "key": "size", + "value": "1000", + "description": "количество событий в наборе" + } + ] + }, + "description": "Обратите внимание: \n- это публичный эндпоинт, соответственно в выдаче должны быть только опубликованные события\n- текстовый поиск (по аннотации и подробному описанию) должен быть без учета регистра букв\n- если в запросе не указан диапазон дат [rangeStart-rangeEnd], то нужно выгружать события, которые произойдут позже текущей даты и времени\n- информация о каждом событии должна включать в себя количество просмотров и количество уже одобренных заявок на участие\n- информацию о том, что по этому эндпоинту был осуществлен и обработан запрос, нужно сохранить в сервисе статистики" + }, + "response": [] + }, + { + "name": "Получение подробной информации об опубликованном событии по его идентификатору", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.collectionVariables.set('response', event);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, views, confirmedRequests, description, participantLimit, state, createdOn, publishedOn, location, requestModeration\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('annotation');\r", + "pm.expect(target).to.have.property('category');\r", + "pm.expect(target).to.have.property('paid');\r", + "pm.expect(target).to.have.property('eventDate');\r", + "pm.expect(target).to.have.property('initiator');\r", + "pm.expect(target).to.have.property('views');\r", + "pm.expect(target).to.have.property('confirmedRequests');\r", + "pm.expect(target).to.have.property('description');\r", + "pm.expect(target).to.have.property('participantLimit');\r", + "pm.expect(target).to.have.property('state');\r", + "pm.expect(target).to.have.property('createdOn');\r", + "pm.expect(target).to.have.property('publishedOn');\r", + "pm.expect(target).to.have.property('location');\r", + "pm.expect(target).to.have.property('requestModeration');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.annotation).equal(target.annotation, 'Аннотация события должна соответствовать аннотации события с указанным идентификатором');\r", + " pm.expect(source.category.id).equal(target.category.id, 'Категория события должна соответствовать категории события с указанным идентификатором');\r", + " pm.expect(source.paid.toString()).equal(target.paid.toString(), 'Стоимость события должна соответствовать стоимости события с указанным идентификатором');\r", + " pm.expect(source.eventDate).equal(target.eventDate, 'Дата проведения события должна соответствовать дате проведения события с указанным идентификатором');\r", + " pm.expect(source.description).equal(target.description, 'Описание события должно соответствовать описанию события с указанным идентификатором');\r", + " pm.expect(source.title).equal(target.title, 'Название события должно соответствовать названию события с указанным идентификатором');\r", + " pm.expect(source.participantLimit.toString()).equal(target.participantLimit.toString(), 'Лимит участников события должен соответствовать лимиту участников события с указанным идентификатором');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/events/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "events", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n- событие должно быть опубликовано\n- информация о событии должна включать в себя количество просмотров и количество подтвержденных запросов\n- информацию о том, что по этому эндпоинту был осуществлен и обработан запрос, нужно сохранить в сервисе статистики" + }, + "response": [] + }, + { + "name": "Получение полной информации о событии добавленном текущим пользователем", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, description, participantLimit, state, createdOn, location, requestModeration\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('annotation');\r", + "pm.expect(target).to.have.property('category');\r", + "pm.expect(target).to.have.property('paid');\r", + "pm.expect(target).to.have.property('eventDate');\r", + "pm.expect(target).to.have.property('initiator');\r", + "pm.expect(target).to.have.property('description');\r", + "pm.expect(target).to.have.property('participantLimit');\r", + "pm.expect(target).to.have.property('state');\r", + "pm.expect(target).to.have.property('createdOn');\r", + "pm.expect(target).to.have.property('location');\r", + "pm.expect(target).to.have.property('requestModeration');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + } + }, + "response": [] + }, + { + "name": "Получение информации о заявках текущего пользователя на участие в чужих событиях", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " const requestToJoin = await api.publishParticipationRequest(event.id, submittedUser.id);\r", + " pm.collectionVariables.set('response', requestToJoin);\r", + " pm.collectionVariables.set('uid', submittedUser.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json()[0];\r", + "\r", + "pm.test(\"Запрос на участие должен содержать поля: id, requester, event, status, created\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('requester');\r", + "pm.expect(target).to.have.property('event');\r", + "pm.expect(target).to.have.property('status');\r", + "pm.expect(target).to.have.property('created');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.id).equal(target.id, 'Идентификатор запроса на участие в событии должен соответствовать идентификатору запроса, созданного ранее');\r", + " pm.expect(source.requester).equal(target.requester, 'Пользователя, запрашивающий доступ на участие в событии, должен соответствовать указанному пользователю');\r", + " pm.expect(source.event).equal(target.event, 'Событие, доступ к которому запрашивает пользователь, должно соответствовать событию, доступ к которому пользователь запрашивал доступ ранее');\r", + " pm.expect(source.created).equal(target.created, 'Время создания запроса на участие в событии должно соответствовать времени создания запроса, созданного ранее указанным пользователем');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/requests", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "requests" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + } + }, + "response": [] + }, + { + "name": "Получение информации о запросах на участие в событии текущего пользователя", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " const requestToJoin = await api.publishParticipationRequest(event.id, submittedUser.id);\r", + " pm.collectionVariables.set('response', requestToJoin);\r", + " pm.collectionVariables.set('uid', user.id);\r", + " pm.collectionVariables.set('eid', event.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json()[0];\r", + "\r", + "pm.test(\"Запрос на участие должен содержать поля: id, requester, event, status, created\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('requester');\r", + "pm.expect(target).to.have.property('event');\r", + "pm.expect(target).to.have.property('status');\r", + "pm.expect(target).to.have.property('created');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.id).equal(target.id, 'Идентификатор запроса на участие в событии должен соответствовать идентификатору запроса на участие в событии указанного пользователя');\r", + " pm.expect(source.requester).equal(target.requester, 'Автор запроса на участие в событии должен соответствовать указанному пользователю');\r", + " pm.expect(source.event).equal(target.event, 'Событие в ответе должно соответствовать событию с запросом на участие от указанного пользователя');\r", + " pm.expect(source.created).equal(target.created, 'Время создания запроса на участие в событии должно соответствовать времени создания запроса на участие в событии указанного пользователя');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/requests", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "requests" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + } + }, + "response": [] + }, + { + "name": "Редактирование данных события и его статуса (отклонение/публикация).", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " let event2 = rnd.getEvent(category.id)\r", + " event2.stateAction = \"PUBLISH_EVENT\"\r", + " pm.collectionVariables.set('response', event2);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: event2,\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, description, participantLimit, state, createdOn, publishedOn, location, requestModeration\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('annotation');\r", + "pm.expect(target).to.have.property('category');\r", + "pm.expect(target).to.have.property('paid');\r", + "pm.expect(target).to.have.property('eventDate');\r", + "pm.expect(target).to.have.property('initiator');\r", + "pm.expect(target).to.have.property('description');\r", + "pm.expect(target).to.have.property('participantLimit');\r", + "pm.expect(target).to.have.property('state');\r", + "pm.expect(target).to.have.property('createdOn');\r", + "pm.expect(target).to.have.property('publishedOn');\r", + "pm.expect(target).to.have.property('location');\r", + "pm.expect(target).to.have.property('requestModeration');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.annotation).equal(target.annotation, 'Аннотация события должна соответствовать искомому событию');\r", + " pm.expect(source.paid.toString()).equal(target.paid.toString(), 'Стоимость события должна соответствовать искомому событию');\r", + " pm.expect(source.eventDate).equal(target.eventDate, 'Дата проведения события должна соответствовать искомому событию');\r", + " pm.expect(source.description).equal(target.description, 'Описание события должно соответствовать искомому событию');\r", + " pm.expect(source.title).equal(target.title, 'Название события должно соответствовать искомому событию');\r", + " pm.expect(source.participantLimit.toString()).equal(target.participantLimit.toString(), 'Лимит участников события должен соответствовать искомому событию');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Изменение события добавленного текущим пользователем", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.collectionVariables.set(\"response\", event);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " stateAction: \"CANCEL_REVIEW\"\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get(\"response\");\r", + "const target = pm.response.json();\r", + "\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, description, participantLimit, state, createdOn, location, requestModeration\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('annotation');\r", + "pm.expect(target).to.have.property('category');\r", + "pm.expect(target).to.have.property('paid');\r", + "pm.expect(target).to.have.property('eventDate');\r", + "pm.expect(target).to.have.property('initiator');\r", + "pm.expect(target).to.have.property('description');\r", + "pm.expect(target).to.have.property('participantLimit');\r", + "pm.expect(target).to.have.property('state');\r", + "pm.expect(target).to.have.property('createdOn');\r", + "pm.expect(target).to.have.property('location');\r", + "pm.expect(target).to.have.property('requestModeration');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.annotation).equal(target.annotation, 'Аннотация отменённого события должна соответствовать аннотации события до отмены');\r", + " pm.expect(source.category.id).equal(target.category.id, 'Категория отменённого события должна соответствовать категории события до отмены');\r", + " pm.expect(source.paid.toString()).equal(target.paid.toString(), 'Стоимость отменённого события должна соответствовать стоимости события до отмены');\r", + " pm.expect(source.eventDate).equal(target.eventDate, 'Дата проведения отменённого события должна соответствовать дате проведения события до отмены');\r", + " pm.expect(source.description).equal(target.description, 'Описание отменённого события должно соответствовать описанию события до отмены');\r", + " pm.expect(source.title).equal(target.title, 'Название отменённого события должно соответствовать названию события до отмены');\r", + " pm.expect(source.participantLimit.toString()).equal(target.participantLimit.toString(), 'Лимит участников отменённого события должен соответствовать лимиту участников события до отмены');\r", + " pm.expect(source.paid.toString()).equal(target.paid.toString(), 'Стоимость отменённого события должна соответствовать стоимости события до отмены');\r", + "});\r", + "\r", + "pm.test(\"Событие должно иметь статус PENDING при создании и статус CANCELED после выполнения запроса\", function () {\r", + " pm.expect(source.state).equal(\"PENDING\");\r", + " pm.expect(target.state).equal(\"CANCELED\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Отмена своего запроса на участие в событии", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " const requestToJoin = await api.publishParticipationRequest(event.id, submittedUser.id);\r", + " pm.collectionVariables.set('response', requestToJoin);\r", + " pm.collectionVariables.set('uid', submittedUser.id);\r", + " pm.collectionVariables.set('reqid', requestToJoin.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "const source = pm.collectionVariables.get('response');\r", + "\r", + "pm.test(\"Запрос на участие должен содержать поля: id, requester, event, status, created\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('requester');\r", + "pm.expect(target).to.have.property('event');\r", + "pm.expect(target).to.have.property('status');\r", + "pm.expect(target).to.have.property('created');\r", + "});\r", + "\r", + "pm.test(\"При создании у запроса на участие должен быть статус PENDING, а при удалении - CANCELED\", function () {\r", + " pm.expect(source.status).equal(\"PENDING\");\r", + " pm.expect(target.status).equal(\"CANCELED\");\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.id).equal(target.id, 'Идентификатор отменённого запроса на участие в событии должен соответствовать идентификатору запроса до отмены');\r", + " pm.expect(source.requester).equal(target.requester, 'Пользователь, отменяющий запрос на участие в событии, должен соответствовать текущему пользователю');\r", + " pm.expect(source.event).equal(target.event, 'Событие отменённого запроса на участие должно соответствовать запросу на участие в событии до отмены');\r", + " pm.expect(source.created).equal(target.created, 'Дата создания отменённого запроса на участие в событии должна соответствовать дате создания запроса до отмены');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/requests/:requestId/cancel", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "requests", + ":requestId", + "cancel" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "requestId", + "value": "{{reqid}}", + "description": "(Required) id запроса на участие" + } + ] + } + }, + "response": [] + }, + { + "name": "Отклонение запроса на участие в событии", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " const requestToJoin = await api.publishParticipationRequest(event.id, submittedUser.id);\r", + " pm.collectionVariables.set('response', requestToJoin);\r", + " pm.collectionVariables.set('uid', user.id);\r", + " pm.collectionVariables.set('eid', event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({requestIds: [requestToJoin.id],\r", + " status:\"REJECTED\"}),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json()[\"rejectedRequests\"][0];\r", + "\r", + "pm.test(\"Запрос на участие должен содержать поля: id, requester, event, status, created\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('requester');\r", + "pm.expect(target).to.have.property('event');\r", + "pm.expect(target).to.have.property('status');\r", + "pm.expect(target).to.have.property('created');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.id).equal(target.id, 'Идентификатор запроса на участие в событии должен соответствовать идентификатору запроса на участие в событии указанного пользователя');\r", + " pm.expect(source.requester).equal(target.requester, 'Пользователь, запрашивающий доступ на участие в событии должен пользователю, отправившему запрос на участие в событии указанного пользователя ранее');\r", + " pm.expect(source.event).equal(target.event, 'Событие, запрос на участие в котором надо подтвердить, должно соответствовать событию указанного пользователя');\r", + " pm.expect(source.created).equal(target.created, 'Время создания запроса на участие в событии после подтверждения должно соответствовать времени создания запроса на участие в событии указанного пользователя до подтверждения');\r", + "});\r", + "\r", + "pm.test(\"Запрос на участие должен иметь статус PENDING при создании и статус REJECTED после выполнения запроса\", function () {\r", + " pm.expect(source.status).equal(\"PENDING\");\r", + " pm.expect(target.status).equal(\"REJECTED\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"requestIds\": [\n 1,\n 2,\n 3\n ],\n \"status\": \"CONFIRMED\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/requests", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "requests" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- если для события лимит заявок равен 0 или отключена пре-модерация заявок, то подтверждение заявок не требуется\n- нельзя подтвердить заявку, если уже достигнут лимит по заявкам на данное событие\n- статус можно изменить только у заявок, находящихся в состоянии ожидания\n- если при подтверждении данной заявки, лимит заявок для события исчерпан, то все неподтверждённые заявки необходимо отклонить" + }, + "response": [] + }, + { + "name": "Изменение статуса (подтверждена, отменена) заявок на участие в событии текущего пользователя", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " const requestToJoin = await api.publishParticipationRequest(event.id, submittedUser.id);\r", + " pm.collectionVariables.set('response', requestToJoin);\r", + " pm.collectionVariables.set('uid', user.id);\r", + " pm.collectionVariables.set('eid', event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({requestIds: [requestToJoin.id],\r", + " status:\"CONFIRMED\"}),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json()[\"confirmedRequests\"][0];\r", + "\r", + "pm.test(\"Запрос на участие должен содержать поля: id, requester, event, status, created\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('requester');\r", + "pm.expect(target).to.have.property('event');\r", + "pm.expect(target).to.have.property('status');\r", + "pm.expect(target).to.have.property('created');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.id).equal(target.id, 'Идентификатор запроса на участие в событии должен соответствовать идентификатору запроса на участие в событии указанного пользователя');\r", + " pm.expect(source.requester).equal(target.requester, 'Пользователь, запрашивающий доступ на участие в событии должен пользователю, отправившему запрос на участие в событии указанного пользователя ранее');\r", + " pm.expect(source.event).equal(target.event, 'Событие, запрос на участие в котором надо подтвердить, должно соответствовать событию указанного пользователя');\r", + " pm.expect(source.created).equal(target.created, 'Время создания запроса на участие в событии после подтверждения должно соответствовать времени создания запроса на участие в событии указанного пользователя до подтверждения');\r", + "});\r", + "\r", + "pm.test(\"Запрос на участие должен иметь статус PENDING при создании и статус CONFIRMED после выполнения запроса\", function () {\r", + " pm.expect(source.status).equal(\"PENDING\");\r", + " pm.expect(target.status).equal(\"CONFIRMED\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/requests", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "requests" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- если для события лимит заявок равен 0 или отключена пре-модерация заявок, то подтверждение заявок не требуется\n- нельзя подтвердить заявку, если уже достигнут лимит по заявкам на данное событие\n- статус можно изменить только у заявок, находящихся в состоянии ожидания\n- если при подтверждении данной заявки, лимит заявок для события исчерпан, то все неподтверждённые заявки необходимо отклонить" + }, + "response": [] + } + ] + }, + { + "name": "Compilation", + "item": [ + { + "name": "Добавление новой подборки", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " compilation = rnd.getCompilation(event.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(compilation),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Подборка должны содержать поля: id, title, pinned, events\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('pinned');\r", + "pm.expect(target).to.have.property('events');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + " pm.expect(target.title).to.be.a(\"string\");\r", + " pm.expect(target.events).to.be.an(\"array\");\r", + "\r", + " pm.expect(source.events[0]).equal(target.events[0].id, 'Идентификаторы событий в подборке должен быть идентичен идентификаторам, указанным при создании подборки ');\r", + " pm.expect(source.title).equal(target.title, 'Название подборки должно соответствовать указанному при создании');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations" + ] + } + }, + "response": [] + }, + { + "name": "Получение подборки событий по её id", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const compilation = await api.addCompilation(rnd.getCompilation());\r", + " pm.collectionVariables.set('response', compilation);\r", + " pm.collectionVariables.set('compid', compilation.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Подборка должны содержать поля: id, title, pinned, events\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('pinned');\r", + "pm.expect(target).to.have.property('events');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.id).equal(target.id, 'Идентификатор подборки должен соответствовать идентификатору подборки добавленной ранее');\r", + " pm.expect(source.title).equal(target.title, 'Название подборки должно соответствовать названию подборки добавленной ранее');\r", + " pm.expect(source.pinned).equal(target.pinned, 'Закреплённость подборки должна соответствовать закреплённости подборки добавленной ранее');\r", + " pm.expect(source.events.join()).equal(target.events.join(), 'События подборки должны соответствовать событиям подборки добавленной ранее');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/compilations/:compId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "compilations", + ":compId" + ], + "variable": [ + { + "key": "compId", + "value": "{{compid}}", + "description": "(Required) id подборки" + } + ] + } + }, + "response": [] + }, + { + "name": "Получение подборок событий", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let newComp = rnd.getCompilation();\r", + " newComp['pinned'] = true;\r", + " const compilation = await api.addCompilation(newComp);\r", + " pm.collectionVariables.set('response', compilation);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json();\r", + "let founded;\r", + "target.forEach(function(element){if (element.id == source.id) founded = element});\r", + "\r", + "pm.test(\"Подборка должны содержать поля: id, title, pinned, events\", function () {\r", + "pm.expect(target[0]).to.have.property('id');\r", + "pm.expect(target[0]).to.have.property('title');\r", + "pm.expect(target[0]).to.have.property('pinned');\r", + "pm.expect(target[0]).to.have.property('events');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.id).equal(founded.id, 'Идентификатор подборки должен соответствовать идентификатору подборки добавленной ранее');\r", + " pm.expect(source.title).equal(founded.title, 'Название подборки должно соответствовать названию подборки добавленной ранее');\r", + " pm.expect(source.pinned).equal(founded.pinned, 'Закрепленность подборки должна соответствовать закрепленности подборки добавленной ранее');\r", + " pm.expect(source.events.join()).equal(founded.events.join(), 'События подборки должны соответствовать событиям подборки добавленной ранее');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/compilations?pinned=true&from=0&size=1000", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "compilations" + ], + "query": [ + { + "key": "pinned", + "value": "true", + "description": "искать только закрепленные/не закрепленные подборки" + }, + { + "key": "from", + "value": "0", + "description": "количество элементов, которые нужно пропустить для формирования текущего набора" + }, + { + "key": "size", + "value": "1000", + "description": "количество элементов в наборе" + } + ] + } + }, + "response": [] + }, + { + "name": "Удаление подборки", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 204\", function () {\r", + " pm.response.to.have.status(204);\r", + "});\r", + "\r", + "let source = pm.collectionVariables.get('response');\r", + "\r", + "pm.test(\"Подборка должна быть найдена до удаления\", function () {\r", + " pm.expect(source).not.to.be.null;\r", + "});\r", + "\r", + "let body\r", + "const req = {\r", + " url: \"http://localhost:8080/compilations?from=0&size=1000\" + pm.collectionVariables.get(\"uid\"),\r", + " method: \"GET\",\r", + " body: body == null ? \"\" : JSON.stringify(body),\r", + " header: { \"Content-Type\": \"application/json\" },\r", + " };\r", + "pm.sendRequest(req, (error, response) => {\r", + " pm.test(\"Подборка должна быть удалена после выполнения запроса\", function(){\r", + " response.json().forEach(element => {\r", + " if(element.id == pm.collectionVariables.get('compid')){\r", + " throw new Error(\"Подборка все еще находится в списке существующих\");\r", + " }\r", + " })\r", + " });\r", + "})\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const compilation = await api.addCompilation(rnd.getCompilation());\r", + " const foundedCompilation = await api.findCompilation(compilation.id);\r", + " pm.collectionVariables.set('compid', compilation.id);\r", + " pm.collectionVariables.set('response', foundedCompilation);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/compilations/:compId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations", + ":compId" + ], + "variable": [ + { + "key": "compId", + "value": "{{compid}}", + "description": "(Required) id подборки" + } + ] + } + }, + "response": [] + }, + { + "name": "Обновить информацию о подборке", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " const compilation = await api.addCompilation(rnd.getCompilation());\r", + " const foundedCompilation = await api.findCompilation(compilation.id);\r", + " pm.collectionVariables.set('compid', compilation.id);\r", + " pm.collectionVariables.set('response', foundedCompilation);\r", + " pm.collectionVariables.set('eid', event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " events : [event.id],\r", + " pinned: true,\r", + " title: rnd.getCompilation().name\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200\", function () {\r", + " pm.response.to.be.ok; \r", + "});\r", + "\r", + "source = pm.collectionVariables.get('response');\r", + "compId = pm.collectionVariables.get('compid');\r", + "eventId = pm.collectionVariables.get('eid');\r", + "\r", + "pm.test(\"Событие не должно быть найдено в подборке до добавления\", function () {\r", + " pm.expect(source.events.length).equal(0);\r", + "});\r", + "\r", + "pm.sendRequest({\r", + " url: pm.collectionVariables.get(\"baseUrl\") + \"/compilations/\" + compId,\r", + " method: 'GET',\r", + " }, (error, response) => {\r", + " \r", + " pm.test(\"Событие должно быть найдено в подборке после добавления\", function () {\r", + " pm.expect(response.json().events[0].id).equal(eventId);\r", + " });\r", + " });" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations/:compId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations", + ":compId" + ], + "variable": [ + { + "key": "compId", + "value": "{{compid}}" + } + ] + } + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "API = class {", + " constructor(postman, verbose = false, baseUrl = \"http://localhost:8080\") {", + " this.baseUrl = baseUrl;", + " this.pm = postman;", + " this._verbose = verbose;", + " }", + "", + " async addUser(user, verbose=null) {", + " return this.post(\"/admin/users\", user, \"Ошибка при добавлении нового пользователя: \", verbose);", + " }", + "", + " async addCategory(category, verbose=null) {", + " return this.post(\"/admin/categories\", category, \"Ошибка при добавлении новой категории: \", verbose);", + " }", + "", + " async addEvent(userId, event, verbose=null) {", + " return this.post(\"/users/\" + userId + \"/events\", event, \"Ошибка при добавлении нового события: \", verbose);", + " }", + "", + " async addCompilation(compilation, verbose=null) {", + " return this.post(\"/admin/compilations\", compilation, \"Ошибка при добавлении новой подборки: \", verbose);", + " }", + "", + " async publishParticipationRequest(eventId, userId, verbose=null) {", + " return this.post('/users/' + userId + '/requests?eventId=' + eventId, null, \"Ошибка при добавлении нового запроса на участие в событии\", verbose);", + " }", + "", + " async publishEvent(eventId, verbose=null) {", + " return this.patch('/admin/events/' + eventId, {stateAction: \"PUBLISH_EVENT\"}, \"Ошибка при публикации события\", verbose);", + " }", + " ", + " async rejectEvent(eventId, verbose=null) {", + " return this.patch('/admin/events/' + eventId, {stateAction: \"REJECT_EVENT\"}, \"Ошибка при отмене события\", verbose);", + " }", + "", + " async acceptParticipationRequest(eventId, userId, reqId, verbose=null) {", + " return this.patch('/users/' + userId + '/events/' + eventId + '/requests', {requestIds:[reqId], status: \"CONFIRMED\"}, \"Ошибка при принятии заявки на участие в событии\", verbose);", + " }", + "", + " async findCategory(catId, verbose=null) {", + " return this.get('/categories/' + catId, null, \"Ошибка при поиске категории по id\", verbose);", + " }", + "", + " async findCompilation(compId, verbose=null) {", + " return this.get('/compilations/' + compId, null, \"Ошибка при поиске подборки по id\", verbose);", + " }", + "", + " async findEvent(eventId, verbose=null) {", + " return this.get('/events/' + eventId, null, \"Ошибка при поиске события по id\", verbose);", + " }", + "", + " async findUser(userId, verbose=null) {", + " return this.get('/admin/users?ids=' + userId, null, \"Ошибка при поиске пользователя по id\", verbose);", + " }", + "", + " async post(path, body, errorText = \"Ошибка при выполнении post-запроса: \", verbose=null) {", + " return this.sendRequest(\"POST\", path, body, errorText, verbose);", + " }", + "", + " async patch(path, body = null, errorText = \"Ошибка при выполнении patch-запроса: \", verbose=null) {", + " return this.sendRequest(\"PATCH\", path, body, errorText, verbose);", + " }", + "", + " async get(path, body = null, errorText = \"Ошибка при выполнении get-запроса: \", verbose=null) {", + " return this.sendRequest(\"GET\", path, body, errorText, verbose);", + " }", + " async sendRequest(method, path, body=null, errorText = \"Ошибка при выполнении запроса: \", verbose=null) {", + " return new Promise((resolve, reject) => {", + " verbose = verbose == null ? this._verbose : verbose;", + " const request = {", + " url: this.baseUrl + path,", + " method: method,", + " body: body == null ? \"\" : JSON.stringify(body),", + " header: { \"Content-Type\": \"application/json\" },", + " };", + " if(verbose) {", + " console.log(\"Отправляю запрос: \", request);", + " }", + "", + " try {", + " this.pm.sendRequest(request, (error, response) => {", + " if(error || (response.code >= 400 && response.code <= 599)) {", + " let err = error ? error : JSON.stringify(response.json());", + " console.error(\"При выполнении запроса к серверу возникла ошика.\\n\", err,", + " \"\\nДля отладки проблемы повторите такой же запрос к вашей программе \" + ", + " \"на локальном компьютере. Данные запроса:\\n\", JSON.stringify(request));", + "", + " reject(new Error(errorText + err));", + " }", + " if(verbose) {", + " console.log(\"Результат обработки запроса: код состояния - \", response.code, \", тело: \", response.json());", + " }", + " if (response.stream.length === 0){", + " reject(new Error('Отправлено пустое тело ответа'))", + " }else{", + " resolve(response.json());", + " }", + " });", + " ", + " } catch(err) {", + " if(verbose) {", + " console.error(errorText, err);", + " }", + " return Promise.reject(err);", + " }", + " });", + " }", + "};", + "", + "RandomUtils = class {", + " constructor() {}", + "", + " getUser() {", + " return {", + " name: pm.variables.replaceIn('{{$randomFullName}}'),", + " email: pm.variables.replaceIn('{{$randomEmail}}')", + " };", + " }", + "", + " getCategory() {", + " return {", + " name: pm.variables.replaceIn('{{$randomWord}}') + Math.floor(Math.random() * 10000 * Math.random()).toString()", + " };", + " }", + "", + " getEvent(categoryId) {", + " return {", + " annotation: pm.variables.replaceIn('{{$randomLoremParagraph}}'),", + " category: categoryId,", + " description: pm.variables.replaceIn('{{$randomLoremParagraphs}}'),", + " eventDate: this.getFutureDateTime(),", + " location: {", + " lat: parseFloat(pm.variables.replaceIn('{{$randomLatitude}}')),", + " lon: parseFloat(pm.variables.replaceIn('{{$randomLongitude}}')),", + " },", + " paid: pm.variables.replaceIn('{{$randomBoolean}}'),", + " participantLimit: pm.variables.replaceIn('{{$randomInt}}'),", + " requestModeration: pm.variables.replaceIn('{{$randomBoolean}}'),", + " title: pm.variables.replaceIn('{{$randomLoremSentence}}'),", + " }", + " }", + "", + " getCompilation(...eventIds) {", + " return {", + " title: pm.variables.replaceIn('{{$randomLoremSentence}}').slice(0, 50),", + " pinned: pm.variables.replaceIn('{{$randomBoolean}}'),", + " events: eventIds", + " };", + " }", + "", + "", + " getFutureDateTime(hourShift = 5, minuteShift=0, yearShift=0) {", + " let moment = require('moment');", + "", + " let m = moment();", + " m.add(hourShift, 'hour');", + " m.add(minuteShift, 'minute');", + " m.add(yearShift, 'year');", + "", + " return m.format('YYYY-MM-DD HH:mm:ss');", + " }", + "", + " getWord(length = 1) {", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const charactersLength = characters.length;", + " let counter = 0;", + " while (counter < length) {", + " result += characters.charAt(Math.floor(Math.random() * charactersLength));", + " counter += 1;", + " }", + " return result;", + " }", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "http://localhost:8080", + "type": "string" + }, + { + "key": "name", + "value": "" + }, + { + "key": "usersCount", + "value": 1, + "type": "number" + }, + { + "key": "catid", + "value": 1, + "type": "number" + }, + { + "key": "request_body", + "value": "" + }, + { + "key": "mail", + "value": "" + }, + { + "key": "response", + "value": "" + }, + { + "key": "uid", + "value": 1, + "type": "number" + }, + { + "key": "catname", + "value": "" + }, + { + "key": "eid", + "value": 1, + "type": "number" + }, + { + "key": "compid", + "value": 1, + "type": "number" + }, + { + "key": "toCheck", + "value": "" + }, + { + "key": "newDataToSet", + "value": "" + }, + { + "key": "uid1", + "value": "" + }, + { + "key": "reqid", + "value": 1, + "type": "number" + }, + { + "key": "catId", + "value": "" + }, + { + "key": "confirmedRequests", + "value": "" + }, + { + "key": "responseArr", + "value": "" + }, + { + "key": "source1", + "value": "" + }, + { + "key": "source2", + "value": "" + }, + { + "key": "fromId", + "value": "0" + }, + { + "key": "source", + "value": "" + } + ] +} diff --git a/.postman/ewm-stat-service.json b/.postman/ewm-stat-service.json new file mode 100644 index 0000000..ba35432 --- /dev/null +++ b/.postman/ewm-stat-service.json @@ -0,0 +1,1027 @@ +{ + "info": { + "_postman_id": "1b12f39d-a514-42d9-99a1-2d2f4026b8e8", + "name": "\"Explore with me\" API статистика", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "23073145", + "_collection_link": "https://universal-shadow-295426.postman.co/workspace/My-Workspace~4200f6aa-0504-44b1-8a1d-707d0dcbd5ce/collection/23073145-1b12f39d-a514-42d9-99a1-2d2f4026b8e8?action=share&creator=23073145&source=collection_link" + }, + "item": [ + { + "name": "Сохранение информации о том, что к эндпоинту был запрос", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let post;\r", + " try {\r", + " post = rnd.getPost();\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(post),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/hit", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "hit" + ] + }, + "description": "Сохранение информации о том, что на uri конкретного сервиса был отправлен запрос пользователем. Название сервиса, uri и ip пользователя указаны в теле запроса." + }, + "response": [] + }, + { + "name": "Получение статистики по посещениям. Обратите внимание: значение даты и времени нужно закодировать (например используя java.net.URLEncoder.encode)(Тест на /events/{id})", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try { \r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " pm.collectionVariables.set(\"uri\", '/events/' + event.id);\r", + "\r", + " pm.sendRequest({\r", + " url : \"http://localhost:8080/events/\" + event.id,\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " pm.sendRequest({\r", + " url : \"http://localhost:9090/stats?start=2020-05-05 00:00:00&end=2035-05-05 00:00:00&uris=/events/\" + event.id.toString() + \"&unique=false\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " pm.collectionVariables.set('source', response.json());\r", + " pm.sendRequest({\r", + " url : \"http://localhost:8080/events/\" + event.id,\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " });\r", + " });\r", + " });\r", + " \r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json()[0];\r", + "\r", + "pm.test(\"Посты должны содержать поля: app, uri, hits\", function () {\r", + " pm.expect(target).to.have.all.keys('app', 'uri', 'hits');\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get(\"source\")[0];\r", + "\r", + "\r", + "pm.test(\"После выполнения запроса GET /events/{id} должно увеличиться количество хитов.\", function(){\r", + " pm.expect(source.hits + 1).equal(target.hits, 'Количество хитов после выполнения запроса GET /events/{id} должно быть больше на 1.');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/stats?start=2020-05-05 00:00:00&end=2035-05-05 00:00:00&uris={{uri}}&unique=false", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "stats" + ], + "query": [ + { + "key": "start", + "value": "2020-05-05 00:00:00", + "description": "(Required) Дата и время начала диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "end", + "value": "2035-05-05 00:00:00", + "description": "(Required) Дата и время конца диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "uris", + "value": "{{uri}}", + "description": "Список uri для которых нужно выгрузить статистику" + }, + { + "key": "unique", + "value": "false", + "description": "Нужно ли учитывать только уникальные посещения (только с уникальным ip)" + } + ] + } + }, + "response": [] + }, + { + "name": "Тест взаимодействия сервисов", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try { \r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event1 = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event1 = await api.publishEvent(event1.id);\r", + " let event2 = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event2 = await api.publishEvent(event2.id);\r", + " pm.collectionVariables.set(\"uri\", '/events/' + event1.id + '&uris=/events/' + event2.id);\r", + "\r", + " pm.sendRequest({\r", + " url : \"http://localhost:8080/events/\" + event1.id,\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " pm.sendRequest({\r", + " url : \"http://localhost:8080/events/\" + event2.id,\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " pm.sendRequest({\r", + " url : \"http://localhost:8080/events/\" + event2.id,\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " });\r", + " });\r", + " });\r", + " \r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Посты должны содержать поля: app, uri, hits\", function () {\r", + " pm.expect(target[0]).to.have.all.keys('app', 'uri', 'hits');\r", + " pm.expect(target[1]).to.have.all.keys('app', 'uri', 'hits');\r", + "});\r", + "\r", + "pm.test(\"В теле ответа должна соблюдаться сортировка по убыванию количества просмотров\", function(){\r", + " pm.expect(target[0].hits).to.be.above(target[1].hits);\r", + "});\r", + "\r", + "pm.test(\"Проверка соответствия реального количества просмотров событий и сохраненных хитов\", function(){\r", + " pm.expect(target[0].hits).equal(2);\r", + " pm.expect(target[1].hits).equal(1);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/stats?start=2020-05-05 00:00:00&end=2035-05-05 00:00:00&uris={{uri}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "stats" + ], + "query": [ + { + "key": "start", + "value": "2020-05-05 00:00:00", + "description": "(Required) Дата и время начала диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "end", + "value": "2035-05-05 00:00:00", + "description": "(Required) Дата и время конца диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "uris", + "value": "{{uri}}", + "description": "Список uri для которых нужно выгрузить статистику" + }, + { + "key": "uris", + "value": "aliqua o", + "description": "Список uri для которых нужно выгрузить статистику", + "disabled": true + }, + { + "key": "unique", + "value": "false", + "description": "Нужно ли учитывать только уникальные посещения (только с уникальным ip)", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Получение статистики по посещениям. Обратите внимание: значение даты и времени нужно закодировать (например используя java.net.URLEncoder.encode)(Тест на /events)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try { \r", + " pm.collectionVariables.set(\"uri\", '/events'); \r", + " \r", + " pm.sendRequest({\r", + " url : \"http://localhost:8080/events\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " \r", + " pm.sendRequest({\r", + " url : \"http://localhost:9090/stats?start=2020-05-05 00:00:00&end=2035-05-05 00:00:00&uris=/events&unique=false\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " pm.collectionVariables.set('source', response.json());\r", + " pm.sendRequest({\r", + " url : \"http://localhost:8080/events\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " });\r", + " });\r", + " });\r", + " \r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json()[0];\r", + "\r", + "pm.test(\"Посты должны содержать поля: app, uri, hits\", function () {\r", + " pm.expect(target).to.have.all.keys('app', 'uri', 'hits');\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get(\"source\")[0];\r", + "\r", + "\r", + "pm.test(\"После выполнения запроса GET /events должно увеличиться количество хитов.\", function(){\r", + " pm.expect(source.hits + 1).equal(target.hits, 'Количество хитов после выполнения запроса GET /events должно быть больше на 1.');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/stats?start=2020-05-05 00:00:00&end=2035-05-05 00:00:00&uris={{uri}}&unique=false", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "stats" + ], + "query": [ + { + "key": "start", + "value": "2020-05-05 00:00:00", + "description": "(Required) Дата и время начала диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "end", + "value": "2035-05-05 00:00:00", + "description": "(Required) Дата и время конца диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "uris", + "value": "{{uri}}", + "description": "Список uri для которых нужно выгрузить статистику" + }, + { + "key": "unique", + "value": "false", + "description": "Нужно ли учитывать только уникальные посещения (только с уникальным ip)" + } + ] + } + }, + "response": [] + }, + { + "name": "Получение статистики по посещениям. (Тест на опциональность и работу параметра unique)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try { \r", + " pm.collectionVariables.set(\"uri\", '/events'); \r", + " \r", + " pm.sendRequest({\r", + " url : \"http://localhost:8080/events\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " pm.sendRequest({\r", + " url : \"http://localhost:8080/events\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " \r", + " pm.sendRequest({\r", + " url : \"http://localhost:9090/stats?start=2020-05-05 00:00:00&end=2035-05-05 00:00:00&uris=/events&unique=true\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " pm.collectionVariables.set('source', response.json());\r", + " });\r", + " });\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json при запросе без опционального параметра unique\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json()[0];\r", + "\r", + "pm.test(\"Посты должны содержать поля: app, uri, hits\", function () {\r", + " pm.expect(target).to.have.all.keys('app', 'uri', 'hits');\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get(\"source\")[0];\r", + "\r", + "pm.test(\"Количество уникальных просмотров с одного ip должно равняться 1\", function () {\r", + " pm.expect(source.hits == 1);\r", + "})\r", + "\r", + "pm.test(\"Количество просмотров с одного ip должно быть больше 1\", function () {\r", + " pm.expect(target.hits > 1);\r", + "})" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/stats?start=2020-05-05 00:00:00&end=2035-05-05 00:00:00&uris={{uri}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "stats" + ], + "query": [ + { + "key": "start", + "value": "2020-05-05 00:00:00", + "description": "(Required) Дата и время начала диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "end", + "value": "2035-05-05 00:00:00", + "description": "(Required) Дата и время конца диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "uris", + "value": "{{uri}}", + "description": "Список uri для которых нужно выгрузить статистику" + }, + { + "key": "uris", + "value": "aliqua o", + "description": "Список uri для которых нужно выгрузить статистику", + "disabled": true + }, + { + "key": "unique", + "value": "false", + "description": "Нужно ли учитывать только уникальные посещения (только с уникальным ip)", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Получение статистики по посещениям. (Тест на верную обработку запроса с неверными датами начала и конца диапазона времени)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400\", function () {\r", + " pm.response.to.be.badRequest;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/stats?start=2035-05-05 00:00:00&end=2020-05-05 00:00:00&uris={{uri}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "stats" + ], + "query": [ + { + "key": "start", + "value": "2035-05-05 00:00:00", + "description": "(Required) Дата и время начала диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "end", + "value": "2020-05-05 00:00:00", + "description": "(Required) Дата и время конца диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "uris", + "value": "{{uri}}", + "description": "Список uri для которых нужно выгрузить статистику" + }, + { + "key": "unique", + "value": "false", + "description": "Нужно ли учитывать только уникальные посещения (только с уникальным ip)", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Тест на верную обработку запроса без даты начала", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400\", function () {\r", + " pm.response.to.be.badRequest;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/stats?end=2020-05-05 00:00:00&uris={{uri}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "stats" + ], + "query": [ + { + "key": "start", + "value": "2035-05-05 00:00:00", + "description": "(Required) Дата и время начала диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")", + "disabled": true + }, + { + "key": "end", + "value": "2020-05-05 00:00:00", + "description": "(Required) Дата и время конца диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "uris", + "value": "{{uri}}", + "description": "Список uri для которых нужно выгрузить статистику" + }, + { + "key": "unique", + "value": "false", + "description": "Нужно ли учитывать только уникальные посещения (только с уникальным ip)", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Тест на верную обработку запроса без даты конца", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400\", function () {\r", + " pm.response.to.be.badRequest;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/stats?start=2035-05-05 00:00:00&uris={{uri}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "stats" + ], + "query": [ + { + "key": "start", + "value": "2035-05-05 00:00:00", + "description": "(Required) Дата и время начала диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "end", + "value": "2020-05-05 00:00:00", + "description": "(Required) Дата и время конца диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")", + "disabled": true + }, + { + "key": "uris", + "value": "{{uri}}", + "description": "Список uri для которых нужно выгрузить статистику" + }, + { + "key": "unique", + "value": "false", + "description": "Нужно ли учитывать только уникальные посещения (только с уникальным ip)", + "disabled": true + } + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "API = class {", + " constructor(postman, verbose = false, baseUrl = \"http://localhost:9090\") {", + " this.baseUrl = baseUrl;", + " this.pm = postman;", + " this._verbose = verbose;", + " }", + "", + " async addPost(post, verbose=null) {", + " return this.post(\"/hit\", post, \"Ошибка при сохранении информации о запросе к эндпойнту: \", verbose);", + " }", + "", + " async addUser(user, verbose=null) {", + " return this.post(\"/admin/users\", user, \"http://localhost:8080\", \"Ошибка при добавлении нового пользователя: \", verbose);", + " }", + "", + " async addCategory(category, verbose=null) {", + " return this.post(\"/admin/categories\", category, \"http://localhost:8080\", \"Ошибка при добавлении новой категории: \", verbose);", + " }", + "", + " async addEvent(userId, event, verbose=null) {", + " return this.post(\"/users/\" + userId + \"/events\", event, \"http://localhost:8080\", \"Ошибка при добавлении нового события: \", verbose);", + " }", + "", + " async publishEvent(eventId, verbose=null) {", + " return this.patch('/admin/events/' + eventId, {stateAction: \"PUBLISH_EVENT\"},\"Ошибка при публикации события\", verbose);", + " }", + "", + " async patch(path, body = null, errorText = \"Ошибка при выполнении patch-запроса: \", verbose=null) {", + " return this.sendRequest(\"PATCH\", path, \"http://localhost:8080\", body, errorText);", + " }", + "", + " async post(path, body, newBaseUrl=null, errorText = \"Ошибка при выполнении post-запроса: \", verbose=null) {", + " return this.sendRequest(\"POST\", path, newBaseUrl, body, errorText);", + " }", + "", + " async sendRequest(method, path, newBaseUrl=null, body=null, errorText = \"Ошибка при выполнении запроса: \", verbose=null) {", + " return new Promise((resolve, reject) => {", + " verbose = verbose == null ? this._verbose : verbose;", + " let request;", + " if (newBaseUrl==null)", + " request = {", + " url: this.baseUrl + path,", + " method: method,", + " body: body == null ? \"\" : JSON.stringify(body),", + " header: { \"Content-Type\": \"application/json\" },", + " };", + " else", + " request = {", + " url: newBaseUrl + path,", + " method: method,", + " body: body == null ? \"\" : JSON.stringify(body),", + " header: { \"Content-Type\": \"application/json\" },", + " };", + "", + " if(verbose) {", + " console.log(\"Отправляю запрос: \", request);", + " }", + "", + " try {", + " this.pm.sendRequest(request, (error, response) => {", + " if(error || (response.code >= 400 && response.code <= 599)) {", + " let err = error ? error : JSON.stringify(response.json());", + " console.error(\"При выполнении запроса к серверу возникла ошика.\\n\", err,", + " \"\\nДля отладки проблемы повторите такой же запрос к вашей программе \" + ", + " \"на локальном компьютере. Данные запроса:\\n\", JSON.stringify(request));", + "", + " reject(new Error(errorText + err));", + " }", + "", + " if(verbose) {", + " console.log(\"Результат обработки запроса: код состояния - \", response.code, \", тело: \", response.json());", + " }", + " try{", + " resolve(response.json());", + " } catch(err){", + " resolve(response);", + " }", + " ", + " });", + " } catch(err) {", + " if(verbose) {", + " console.error(errorText, err);", + " }", + " return Promise.reject(err);", + " }", + " });", + " }", + "};", + "", + "RandomUtils = class {", + " constructor() {}", + "", + " getPost() {", + " return {", + " app: \"ewm-main-service\",", + " uri: \"/events/\" + pm.variables.replaceIn('{{$randomInt}}'),", + " ip: pm.variables.replaceIn('{{$randomIP}}'),", + " timestamp: this.getPastDateTime()", + " }", + " }", + "", + " getUser() {", + " return {", + " name: pm.variables.replaceIn('{{$randomFullName}}'),", + " email: pm.variables.replaceIn('{{$randomEmail}}')", + " };", + " }", + "", + " getCategory() {", + " return {", + " name: pm.variables.replaceIn('{{$randomWord}}') + Math.floor(Math.random() * 100).toString()", + " };", + " }", + "", + " getEvent(categoryId) {", + " return {", + " annotation: pm.variables.replaceIn('{{$randomLoremParagraph}}'),", + " category: categoryId,", + " description: pm.variables.replaceIn('{{$randomLoremParagraphs}}'),", + " eventDate: this.getFutureDateTime(),", + " location: {", + " lat: parseFloat(pm.variables.replaceIn('{{$randomLatitude}}')),", + " lon: parseFloat(pm.variables.replaceIn('{{$randomLongitude}}')),", + " },", + " paid: pm.variables.replaceIn('{{$randomBoolean}}'),", + " participantLimit: pm.variables.replaceIn('{{$randomInt}}'),", + " requestModeration: pm.variables.replaceIn('{{$randomBoolean}}'),", + " title: pm.variables.replaceIn('{{$randomLoremSentence}}'),", + " }", + " }", + " ", + " getCompilation(...eventIds) { ", + " return { ", + " title: pm.variables.replaceIn('{{$randomLoremSentence}}'), ", + " pinned: pm.variables.replaceIn('{{$randomBoolean}}'), ", + " events: eventIds ", + " }; ", + " }", + "", + " getPastDateTime(hourShift = 5, minuteShift=0, yearShift=0) {", + " let moment = require('moment');", + "", + " let m = moment();", + " m.subtract(hourShift, 'hour');", + " m.subtract(minuteShift, 'minute');", + " m.subtract(yearShift, 'year');", + "", + " return m.format('YYYY-MM-DD HH:mm:ss');", + " }", + "", + " getFutureDateTime(hourShift = 5, minuteShift=0, yearShift=0) {", + " let moment = require('moment');", + "", + " let m = moment();", + " m.add(hourShift, 'hour');", + " m.add(minuteShift, 'minute');", + " m.add(yearShift, 'year');", + "", + " return m.format('YYYY-MM-DD HH:mm:ss');", + " }", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "http://localhost:9090", + "type": "string" + }, + { + "key": "uri", + "value": "1" + }, + { + "key": "source", + "value": "" + } + ] +} diff --git a/.postman/feature.json b/.postman/feature.json new file mode 100644 index 0000000..f75e49e --- /dev/null +++ b/.postman/feature.json @@ -0,0 +1,1057 @@ +{ + "info": { + "_postman_id": "3f1bf201-4169-477e-89f3-801890a0f950", + "name": "feature_rating_events", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "50204807", + "_collection_link": "https://aleksobm-7059599.postman.co/workspace/Team's-Workspace~91ebb0d8-1476-4e40-85ec-9b22813a2ce5/collection/50204807-3f1bf201-4169-477e-89f3-801890a0f950?action=share&source=collection_link&creator=50204807" + }, + "item": [ + { + "name": "Поставить лайк событию", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const initiator = await api.addUser(rnd.getUser());\r", + "\r", + " const category = await api.addCategory(rnd.getCategory());\r", + "\r", + " let event = await api.addEvent(initiator.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + "\r", + " event = await api.publishEvent(event.id);\r", + "\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201); \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Ответ должен содержать поля: id, userId, eventId, reaction\", function () {\r", + " pm.expect(target).to.have.property('id');\r", + " pm.expect(target).to.have.property('userId');\r", + " pm.expect(target).to.have.property('eventId');\r", + " pm.expect(target).to.have.property('reaction');\r", + "});\r", + "\r", + "pm.test(\"Проверка данных в полях ответа\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + " pm.expect(target.userId).to.equal(pm.collectionVariables.get('uid'));\r", + " pm.expect(target.eventId).to.equal(pm.collectionVariables.get('eid'));\r", + " pm.expect(target.reaction).to.equal('LIKE');\r", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"reaction\" : \"LIKE\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/likes", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "likes" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id текущего события" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Поставить дизлайк событию", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const initiator = await api.addUser(rnd.getUser());\r", + "\r", + " const category = await api.addCategory(rnd.getCategory());\r", + "\r", + " let event = await api.addEvent(initiator.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + "\r", + " event = await api.publishEvent(event.id);\r", + "\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201); \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Ответ должен содержать поля: id, userId, eventId, reaction\", function () {\r", + " pm.expect(target).to.have.property('id');\r", + " pm.expect(target).to.have.property('userId');\r", + " pm.expect(target).to.have.property('eventId');\r", + " pm.expect(target).to.have.property('reaction');\r", + "});\r", + "\r", + "pm.test(\"Проверка данных в полях ответа\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + " pm.expect(target.userId).to.equal(pm.collectionVariables.get('uid'));\r", + " pm.expect(target.eventId).to.equal(pm.collectionVariables.get('eid'));\r", + " pm.expect(target.reaction).to.equal('DISLIKE');\r", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"reaction\" : \"DISLIKE\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/likes", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "likes" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id текущего события" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Получение события после добавления лайка", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const initiator = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", initiator.id)\r", + "\r", + " const category = await api.addCategory(rnd.getCategory());\r", + "\r", + " let event = await api.addEvent(initiator.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + "\r", + " event = await api.publishEvent(event.id);\r", + " \r", + " const user = await api.addUser(rnd.getUser());\r", + " const like = await api.addLike(user.id, event.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, 'initiator', 'location', 'confirmedRequests', 'requestModeration', 'publishedOn', 'state', 'views', 'rate'\", function () {\r", + " pm.expect(target).to.contain.keys('id', 'title', 'annotation', 'category', 'paid', 'eventDate', 'initiator', 'location', 'confirmedRequests', 'requestModeration', 'publishedOn', 'state', 'views', 'rate');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + "});\r", + "\r", + "pm.test(\"Поле rate должно быть равно 1\", function () {\r", + " pm.expect(target.rate).to.equal(1);\r", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id текущего события" + } + ] + } + }, + "response": [] + }, + { + "name": "Получение события после добавления дизлайка", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const initiator = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", initiator.id)\r", + "\r", + " const category = await api.addCategory(rnd.getCategory());\r", + "\r", + " let event = await api.addEvent(initiator.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + "\r", + " event = await api.publishEvent(event.id);\r", + " \r", + " const user = await api.addUser(rnd.getUser());\r", + " const dislike = await api.addDislike(user.id, event.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, 'initiator', 'location', 'confirmedRequests', 'requestModeration', 'publishedOn', 'state', 'views', 'rate'\", function () {\r", + " pm.expect(target).to.contain.keys('id', 'title', 'annotation', 'category', 'paid', 'eventDate', 'initiator', 'location', 'confirmedRequests', 'requestModeration', 'publishedOn', 'state', 'views', 'rate');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + "});\r", + "\r", + "pm.test(\"Поле rate должно быть равно -1\", function () {\r", + " pm.expect(target.rate).to.equal(-1);\r", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id текущего события" + } + ] + } + }, + "response": [] + }, + { + "name": "Удаление лайка", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const initiator = await api.addUser(rnd.getUser());\r", + "\r", + " const category = await api.addCategory(rnd.getCategory());\r", + "\r", + " let event = await api.addEvent(initiator.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + "\r", + " event = await api.publishEvent(event.id);\r", + "\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set('uid', user.id);\r", + "\r", + " await api.addLike(user.id, event.id);\r", + " const likedEvent = await api.findEvent(event.id);\r", + " if (likedEvent.rate !== 1) {\r", + " throw \"Ошибка: Ожидается, что лайк увеличит рейтинг события до 1, но фактически рейтинг не изменился.\"\r", + " }\r", + "\r", + " } catch (err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => { }, 1000);\r", + "\r", + "setTimeout(async () => {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + "},\r", + " 100\r", + ");" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.success;\r", + "});\r", + "\r", + "\r", + "\r", + "\r", + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/likes", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "likes" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id текущего события" + } + ] + } + }, + "response": [] + }, + { + "name": "Попытка поставить лайк своему событию", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const initiator = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", initiator.id);\r", + "\r", + " const category = await api.addCategory(rnd.getCategory());\r", + "\r", + " let event = await api.addEvent(initiator.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + "\r", + " event = await api.publishEvent(event.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(400);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"reaction\" : \"LIKE\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/likes", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "likes" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id текущего события" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Попытка поставить лайк не опубликованному событию", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const initiator = await api.addUser(rnd.getUser());\r", + "\r", + " const category = await api.addCategory(rnd.getCategory());\r", + "\r", + " let event = await api.addEvent(initiator.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + "\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(404);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"reaction\" : \"LIKE\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/likes", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "likes" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id текущего события" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "requests": {}, + "exec": [ + "API = class {", + " constructor(postman, verbose = false, baseUrl = \"http://localhost:8080\") {", + " this.baseUrl = baseUrl;", + " this.pm = postman;", + " this._verbose = verbose;", + " }", + "", + " async addLike(userId, eventId, verbose=null) {", + " let reaction = {\"reaction\" : \"LIKE\"};", + " return this.post(\"/users/\" + userId + \"/events/\" + eventId + \"/likes\", reaction, \"Ошибка при добавлении лайка: \", verbose)", + " }", + "", + " async addDislike(userId, eventId, verbose=null) {", + " let reaction = {\"reaction\" : \"DISLIKE\"};", + " return this.post(\"/users/\" + userId + \"/events/\" + eventId + \"/likes\", reaction, \"Ошибка при добавлении лайка: \", verbose)", + " }", + "", + " async addUser(user, verbose=null) {", + " return this.post(\"/admin/users\", user, \"Ошибка при добавлении нового пользователя: \", verbose);", + " }", + "", + " async addCategory(category, verbose=null) {", + " return this.post(\"/admin/categories\", category, \"Ошибка при добавлении новой категории: \", verbose);", + " }", + "", + " async addEvent(userId, event, verbose=null) {", + " return this.post(\"/users/\" + userId + \"/events\", event, \"Ошибка при добавлении нового события: \", verbose);", + " }", + "", + " async addCompilation(compilation, verbose=null) {", + " return this.post(\"/admin/compilations\", compilation, \"Ошибка при добавлении новой подборки: \", verbose);", + " }", + "", + " async publishParticipationRequest(eventId, userId, verbose=null) {", + " return this.post('/users/' + userId + '/requests?eventId=' + eventId, null, \"Ошибка при добавлении нового запроса на участие в событии\", verbose);", + " }", + "", + " async publishEvent(eventId, verbose=null) {", + " return this.patch('/admin/events/' + eventId, {stateAction: \"PUBLISH_EVENT\"}, \"Ошибка при публикации события\", verbose);", + " }", + " ", + " async rejectEvent(eventId, verbose=null) {", + " return this.patch('/admin/events/' + eventId, {stateAction: \"REJECT_EVENT\"}, \"Ошибка при отмене события\", verbose);", + " }", + "", + " async acceptParticipationRequest(eventId, userId, reqId, verbose=null) {", + " return this.patch('/users/' + userId + '/events/' + eventId + '/requests', {requestIds:[reqId], status: \"CONFIRMED\"}, \"Ошибка при принятии заявки на участие в событии\", verbose);", + " }", + "", + " async findCategory(catId, verbose=null) {", + " return this.get('/categories/' + catId, null, \"Ошибка при поиске категории по id\", verbose);", + " }", + "", + " async findCompilation(compId, verbose=null) {", + " return this.get('/compilations/' + compId, null, \"Ошибка при поиске подборки по id\", verbose);", + " }", + "", + " async findEvent(eventId, verbose=null) {", + " return this.get('/events/' + eventId, null, \"Ошибка при поиске события по id\", verbose);", + " }", + "", + " async findUser(userId, verbose=null) {", + " return this.get('/admin/users?ids=' + userId, null, \"Ошибка при поиске пользователя по id\", verbose);", + " }", + "", + " async post(path, body, errorText = \"Ошибка при выполнении post-запроса: \", verbose=null) {", + " return this.sendRequest(\"POST\", path, body, errorText, verbose);", + " }", + "", + " async patch(path, body = null, errorText = \"Ошибка при выполнении patch-запроса: \", verbose=null) {", + " return this.sendRequest(\"PATCH\", path, body, errorText, verbose);", + " }", + "", + " async get(path, body = null, errorText = \"Ошибка при выполнении get-запроса: \", verbose=null) {", + " return this.sendRequest(\"GET\", path, body, errorText, verbose);", + " }", + " async sendRequest(method, path, body=null, errorText = \"Ошибка при выполнении запроса: \", verbose=null) {", + " return new Promise((resolve, reject) => {", + " verbose = verbose == null ? this._verbose : verbose;", + " const request = {", + " url: this.baseUrl + path,", + " method: method,", + " body: body == null ? \"\" : JSON.stringify(body),", + " header: { \"Content-Type\": \"application/json\" },", + " };", + " if(verbose) {", + " console.log(\"Отправляю запрос: \", request);", + " }", + "", + " try {", + " this.pm.sendRequest(request, (error, response) => {", + " if(error || (response.code >= 400 && response.code <= 599)) {", + " let err = error ? error : JSON.stringify(response.json());", + " console.error(\"При выполнении запроса к серверу возникла ошика.\\n\", err,", + " \"\\nДля отладки проблемы повторите такой же запрос к вашей программе \" + ", + " \"на локальном компьютере. Данные запроса:\\n\", JSON.stringify(request));", + "", + " reject(new Error(errorText + err));", + " }", + " if(verbose) {", + " console.log(\"Результат обработки запроса: код состояния - \", response.code, \", тело: \", response.json());", + " }", + " if (response.stream.length === 0){", + " reject(new Error('Отправлено пустое тело ответа'))", + " }else{", + " resolve(response.json());", + " }", + " });", + " ", + " } catch(err) {", + " if(verbose) {", + " console.error(errorText, err);", + " }", + " return Promise.reject(err);", + " }", + " });", + " }", + "};", + "", + "RandomUtils = class {", + " constructor() {}", + "", + " getUser() {", + " return {", + " name: pm.variables.replaceIn('{{$randomFullName}}'),", + " email: pm.variables.replaceIn('{{$randomEmail}}')", + " };", + " }", + "", + " getCategory() {", + " return {", + " name: pm.variables.replaceIn('{{$randomWord}}') + Math.floor(Math.random() * 10000 * Math.random()).toString()", + " };", + " }", + "", + " getEvent(categoryId) {", + " return {", + " annotation: pm.variables.replaceIn('{{$randomLoremParagraph}}'),", + " category: categoryId,", + " description: pm.variables.replaceIn('{{$randomLoremParagraphs}}'),", + " eventDate: this.getFutureDateTime(),", + " location: {", + " lat: parseFloat(pm.variables.replaceIn('{{$randomLatitude}}')),", + " lon: parseFloat(pm.variables.replaceIn('{{$randomLongitude}}')),", + " },", + " paid: pm.variables.replaceIn('{{$randomBoolean}}'),", + " participantLimit: pm.variables.replaceIn('{{$randomInt}}'),", + " requestModeration: pm.variables.replaceIn('{{$randomBoolean}}'),", + " title: pm.variables.replaceIn('{{$randomLoremSentence}}'),", + " }", + " }", + "", + " getCompilation(...eventIds) {", + " return {", + " title: pm.variables.replaceIn('{{$randomLoremSentence}}').slice(0, 50),", + " pinned: pm.variables.replaceIn('{{$randomBoolean}}'),", + " events: eventIds", + " };", + " }", + "", + "", + " getFutureDateTime(hourShift = 5, minuteShift=0, yearShift=0) {", + " let moment = require('moment');", + "", + " let m = moment();", + " m.add(hourShift, 'hour');", + " m.add(minuteShift, 'minute');", + " m.add(yearShift, 'year');", + "", + " return m.format('YYYY-MM-DD HH:mm:ss');", + " }", + "", + " getWord(length = 1) {", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const charactersLength = characters.length;", + " let counter = 0;", + " while (counter < length) {", + " result += characters.charAt(Math.floor(Math.random() * charactersLength));", + " counter += 1;", + " }", + " return result;", + " }", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "requests": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "http://localhost:8080" + }, + { + "key": "uid", + "value": "" + }, + { + "key": "eid", + "value": "" + } + ] +} \ No newline at end of file diff --git a/core/main-service/pom.xml b/core/main-service/pom.xml new file mode 100644 index 0000000..2098d21 --- /dev/null +++ b/core/main-service/pom.xml @@ -0,0 +1,131 @@ + + + 4.0.0 + + + ru.practicum + core + 0.0.1-SNAPSHOT + + + main-service + jar + + + 21 + 21 + UTF-8 + + + + + ru.practicum + stat-dto + 0.0.1-SNAPSHOT + + + ru.practicum + stat-client + 0.0.1-SNAPSHOT + + + org.projectlombok + lombok + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-test + test + + + org.zalando + logbook-spring-boot-starter + 4.0.3 + + + org.postgresql + postgresql + runtime + + + com.h2database + h2 + runtime + + + io.hypersistence + hypersistence-utils-hibernate-63 + 3.15.2 + compile + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.cloud + spring-cloud-starter-config + + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + com.github.ben-manes.caffeine + caffeine + + + + org.springframework.retry + spring-retry + + + org.springframework.boot + spring-boot-starter-aop + + + + org.springframework.boot + spring-boot-starter-actuator + + + io.micrometer + micrometer-registry-prometheus + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + -Duser.language=ru -Duser.country=RU + + + + + + \ No newline at end of file diff --git a/core/main-service/src/main/java/ru/practicum/ewm/MainServiceApp.java b/core/main-service/src/main/java/ru/practicum/ewm/MainServiceApp.java new file mode 100644 index 0000000..eab9033 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/MainServiceApp.java @@ -0,0 +1,13 @@ +package ru.practicum.ewm; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +@SpringBootApplication +@ComponentScan(value = {"ru.practicum.ewm", "ru.practicum.stat.client"}) +public class MainServiceApp { + public static void main(String[] args) { + SpringApplication.run(MainServiceApp.class, args); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/controller/admin/AdminCategoryController.java b/core/main-service/src/main/java/ru/practicum/ewm/controller/admin/AdminCategoryController.java new file mode 100644 index 0000000..341ae49 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/controller/admin/AdminCategoryController.java @@ -0,0 +1,57 @@ +package ru.practicum.ewm.controller.admin; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.ewm.dto.category.CategoryDto; +import ru.practicum.ewm.dto.category.NewCategoryDto; +import ru.practicum.ewm.service.category.CategoryService; + +@RestController +@RequestMapping(path = "/admin/categories") +@RequiredArgsConstructor +public class AdminCategoryController { + + private final CategoryService categoryService; + + /** + * Добавление новой категории. Обратите внимание: имя категории должно быть уникальным + * + * @param newCategoryDto данные добавляемой категории + * @return CategoryDto + */ + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public CategoryDto addNewCategory(@RequestBody @Valid NewCategoryDto newCategoryDto) { + return categoryService.adminAddNewCategory(newCategoryDto); + } + + /** + * Изменение категории + *

+ * Обратите внимание: имя категории должно быть уникальным + * + * @param catId id категории + * @param categoryDto Данные категории для изменения + * @return CategoryDto + */ + @PatchMapping("/{catId}") + public CategoryDto updateCategory(@PathVariable Long catId, + @RequestBody @Valid CategoryDto categoryDto) { + return categoryService.updateCategory(catId, categoryDto); + } + + /** + * Удаление категории + *

+ * Обратите внимание: с категорией не должно быть связано ни одного события. + * + * @param catId id категории + */ + @DeleteMapping("/{catId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteCategory(@PathVariable Long catId) { + categoryService.deleteCategory(catId); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/controller/admin/AdminCompilationController.java b/core/main-service/src/main/java/ru/practicum/ewm/controller/admin/AdminCompilationController.java new file mode 100644 index 0000000..a68b957 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/controller/admin/AdminCompilationController.java @@ -0,0 +1,53 @@ +package ru.practicum.ewm.controller.admin; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.ewm.dto.compilation.CompilationDto; +import ru.practicum.ewm.dto.compilation.CompilationUpdateDto; +import ru.practicum.ewm.dto.compilation.NewCompilationDto; +import ru.practicum.ewm.service.compilation.CompilationService; + +@RestController +@RequestMapping(path = "/admin/compilations") +@RequiredArgsConstructor +public class AdminCompilationController { + + private final CompilationService compilationService; + + /** + * Добавление новой подборки (подборка может не содержать событий) + * + * @param newCompilationDto данные новой подборки + * @return {@link CompilationDto} + */ + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public CompilationDto createCompilation(@RequestBody @Valid NewCompilationDto newCompilationDto) { + return compilationService.addCompilation(newCompilationDto); + } + + /** + * + * @param compilationUpdateDto данные для обновления подборки + * @param compId id подборки + * @return {@link CompilationDto} + */ + @PatchMapping("/{compId}") + public CompilationDto updateCompilation(@RequestBody @Valid CompilationUpdateDto compilationUpdateDto, + @PathVariable Long compId) { + return compilationService.updateCompilation(compId, compilationUpdateDto); + } + + /** + * Удаление подборки + * + * @param compId id подборки + */ + @DeleteMapping("/{compId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delById(@PathVariable Long compId) { + compilationService.delById(compId); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/controller/admin/AdminEventController.java b/core/main-service/src/main/java/ru/practicum/ewm/controller/admin/AdminEventController.java new file mode 100644 index 0000000..9aafe96 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/controller/admin/AdminEventController.java @@ -0,0 +1,82 @@ +package ru.practicum.ewm.controller.admin; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.*; +import ru.practicum.ewm.dto.event.AdminGetDto; +import ru.practicum.ewm.dto.event.EventFullDto; +import ru.practicum.ewm.dto.event.UpdateEventAdminRequest; +import ru.practicum.ewm.model.enums.EventState; +import ru.practicum.ewm.service.event.EventService; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping(path = "/admin/events") +@RequiredArgsConstructor +public class AdminEventController { + + private final EventService eventService; + + /** + * Поиск событий + *

+ * Эндпоинт возвращает полную информацию обо всех событиях подходящих под переданные условия + * В случае, если по заданным фильтрам не найдено ни одного события, возвращает пустой список + * + * @param users список id пользователей, чьи события нужно найти + * @param states список состояний в которых находятся искомые события + * @param categories список id категорий в которых будет вестись поиск + * @param rangeStart дата и время не раньше которых должно произойти событие + * @param rangeEnd дата и время не позже которых должно произойти событие + * @param from количество событий, которые нужно пропустить для формирования текущего набора + * Default value : 0 + * @param size количество событий в наборе + * Default value : 10 + * @return List<{@link EventFullDto}> + */ + @GetMapping + public List adminGetEvents( + @RequestParam(required = false) List users, + @RequestParam(required = false) List states, + @RequestParam(required = false) List categories, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime rangeStart, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime rangeEnd, + @RequestParam(required = false, defaultValue = "0") Integer from, + @RequestParam(required = false, defaultValue = "10") Integer size) { + + AdminGetDto getDto = AdminGetDto.builder() + .users(users) + .states(states) + .categories(categories) + .rangeStart(rangeStart) + .rangeEnd(rangeEnd) + .from(from) + .size(size) + .build(); + + return eventService.adminGetEvents(getDto); + } + + /** + * Редактирование данных события и его статуса (отклонение/публикация) + *

+ * Редактирование данных любого события администратором. Валидация данных не требуется. Обратите внимание: + * - дата начала изменяемого события должна быть не ранее чем за час от даты публикации. (Ожидается код ошибки 409) + * - событие можно публиковать, только если оно в состоянии ожидания публикации (Ожидается код ошибки 409) + * - событие можно отклонить, только если оно еще не опубликовано (Ожидается код ошибки 409) + * + * @param eventId id события + * @param request Данные HTTP-запроса + * @return {@link EventFullDto} + */ + @PatchMapping("/{eventId}") + public EventFullDto adminUpdateEvent( + @PathVariable Long eventId, + @RequestBody @Valid UpdateEventAdminRequest request) { + + return eventService.adminUpdateEvent(eventId, request); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/controller/admin/AdminUserController.java b/core/main-service/src/main/java/ru/practicum/ewm/controller/admin/AdminUserController.java new file mode 100644 index 0000000..cc5a305 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/controller/admin/AdminUserController.java @@ -0,0 +1,65 @@ +package ru.practicum.ewm.controller.admin; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.ewm.dto.user.NewUserRequest; +import ru.practicum.ewm.dto.user.UserDto; +import ru.practicum.ewm.service.user.UserService; + +import java.util.List; + +@RestController +@RequestMapping(path = "/admin/users") +@RequiredArgsConstructor +public class AdminUserController { + + private final UserService userService; + + /** + * Добавление нового пользователя + * + * @param newUserRequest Данные добавляемого пользователя + * @return {@link UserDto} + */ + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public UserDto addNewUser(@RequestBody @Valid NewUserRequest newUserRequest) { + return userService.adminAddNewUser(newUserRequest); + } + + /** + * Получение информации о пользователях + *

+ * Возвращает информацию обо всех пользователях (учитываются параметры ограничения выборки), + * либо о конкретных (учитываются указанные идентификаторы) + * В случае, если по заданным фильтрам не найдено ни одного пользователя, возвращает пустой список + * + * @param ids id пользователей + * @param from количество элементов, которые нужно пропустить для формирования текущего набора + * Default value : 0 + * @param size количество элементов в наборе + * Default value : 10 + * @return List<{@link UserDto}> + */ + @GetMapping + public List getUsers( + @RequestParam(required = false) List ids, + @RequestParam(defaultValue = "0") int from, + @RequestParam(defaultValue = "10") int size + ) { + return userService.getUsers(ids, from, size); + } + + /** + * Удаление пользователя + * + * @param userId id пользователя + */ + @DeleteMapping("/{userId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteUser(@PathVariable Long userId) { + userService.deleteUser(userId); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/controller/free/FreeCategoryController.java b/core/main-service/src/main/java/ru/practicum/ewm/controller/free/FreeCategoryController.java new file mode 100644 index 0000000..f3eb20b --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/controller/free/FreeCategoryController.java @@ -0,0 +1,50 @@ +package ru.practicum.ewm.controller.free; + +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import ru.practicum.ewm.dto.category.CategoryDto; +import ru.practicum.ewm.service.category.CategoryService; + +import java.util.List; + +@Slf4j +@RestController +@RequestMapping(path = "/categories") +@RequiredArgsConstructor +public class FreeCategoryController { + + private final CategoryService categoryService; + + /** + * Получение категорий. + *

+ * В случае, если по заданным фильтрам не найдено ни одной категории, возвращает пустой список + * + * @param from количество категорий, которые нужно пропустить для формирования текущего набора + * Default value : 0 + * @param size количество категорий в наборе + * Default value : 10 + * @return List<{@link CategoryDto}> + */ + @GetMapping + public List findAll(@RequestParam(defaultValue = "0") @PositiveOrZero Integer from, + @RequestParam(defaultValue = "10") @Positive Integer size) { + return categoryService.findAll(from, size); + } + + /** + * Получение информации о категории по её идентификатору. + *

+ * В случае, если категории с заданным id не найдено, возвращает статус код 404 + * + * @param catId id категории + * @return {@link CategoryDto} + */ + @GetMapping("/{catId}") + public CategoryDto findById(@PathVariable @Positive Long catId) { + return categoryService.findById(catId); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/controller/free/FreeCompilationController.java b/core/main-service/src/main/java/ru/practicum/ewm/controller/free/FreeCompilationController.java new file mode 100644 index 0000000..40450ac --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/controller/free/FreeCompilationController.java @@ -0,0 +1,60 @@ +package ru.practicum.ewm.controller.free; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.practicum.ewm.dto.compilation.CompilationDto; +import ru.practicum.ewm.dto.compilation.CompilationSearchFilter; +import ru.practicum.ewm.service.compilation.CompilationService; + +import java.util.List; + +@RestController +@RequestMapping(path = "/compilations") +@RequiredArgsConstructor +public class FreeCompilationController { + + private final CompilationService compilationService; + + /** + * Получение подборок событий + *

+ * В случае, если по заданным фильтрам не найдено ни одной подборки, возвращает пустой список + * + * @param pinned искать только закрепленные/не закрепленные подборки + * @param from количество элементов, которые нужно пропустить для формирования текущего набора + * Default value : 0 + * @param size количество элементов в наборе + * Default value : 10 + * @param request Данные HTTP-запроса + * @return List<{@link CompilationDto}> + */ + @GetMapping + public List getCompilations( + @RequestParam(required = false) Boolean pinned, + @RequestParam(defaultValue = "0") Integer from, + @RequestParam(defaultValue = "10") Integer size, + HttpServletRequest request) { + + CompilationSearchFilter filter = CompilationSearchFilter.builder() + .pinned(pinned) + .from(from) + .size(size) + .build(); + + return compilationService.getByFilter(filter, request); + } + + /** + * Получение подборки событий по его id + * + * @param compId id подборки + * @param request Данные HTTP-запроса + * @return {@link CompilationDto} + */ + @GetMapping("/{compId}") + public CompilationDto getCompilationById(@PathVariable Long compId, + HttpServletRequest request) { + return compilationService.getById(compId, request); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/controller/free/FreeEventController.java b/core/main-service/src/main/java/ru/practicum/ewm/controller/free/FreeEventController.java new file mode 100644 index 0000000..996383e --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/controller/free/FreeEventController.java @@ -0,0 +1,89 @@ +package ru.practicum.ewm.controller.free; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.*; +import ru.practicum.ewm.dto.event.EventFullDto; +import ru.practicum.ewm.dto.event.EventShortDto; +import ru.practicum.ewm.dto.event.FreeGetDto; +import ru.practicum.ewm.service.event.EventService; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping(path = "/events") +@RequiredArgsConstructor +public class FreeEventController { + + private final EventService eventService; + + /** + * Получение событий с возможностью фильтрации + *

+ * Обратите внимание: + * - это публичный эндпоинт, соответственно в выдаче должны быть только опубликованные события + * - текстовый поиск (по аннотации и подробному описанию) должен быть без учета регистра букв + * - если в запросе не указан диапазон дат [rangeStart-rangeEnd], то нужно выгружать события, + * которые произойдут позже текущей даты и времени + * - информация о каждом событии должна включать в себя количество просмотров и количество + * уже одобренных заявок на участие + * - информацию о том, что по этому эндпоинту был осуществлен и обработан запрос, нужно + * сохранить в сервисе статистики + * В случае, если по заданным фильтрам не найдено ни одного события, возвращает пустой список + * + * @param text текст для поиска в содержимом аннотации и подробном описании события + * @param categories список идентификаторов категорий в которых будет вестись поиск + * @param paid поиск только платных/бесплатных событий + * @param rangeStart дата и время не раньше которых должно произойти событие + * @param rangeEnd дата и время не позже которых должно произойти событие + * @param onlyAvailable только события у которых не исчерпан лимит запросов на участие Default value : false + * @param sort Вариант сортировки: по дате события или по количеству просмотров Available values : EVENT_DATE, VIEWS + * @param from количество событий, которые нужно пропустить для формирования текущего набора Default value : 0 + * @param size количество событий в наборе Default value : 10 + * @param request Данные HTTP-запроса + * @return List<{@link EventShortDto}> + */ + @GetMapping + public List getFreeEvents( + @RequestParam(required = false) String text, + @RequestParam(required = false) List categories, + @RequestParam(required = false) Boolean paid, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime rangeStart, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime rangeEnd, + @RequestParam(required = false, defaultValue = "false") Boolean onlyAvailable, + @RequestParam(required = false) FreeGetDto.FreeEventSort sort, + @RequestParam(required = false, defaultValue = "0") Integer from, + @RequestParam(required = false, defaultValue = "10") Integer size, + HttpServletRequest request + ) { + + FreeGetDto freeGetDto = FreeGetDto.builder() + .text(text) + .categories(categories) + .paid(paid) + .rangeStart(rangeStart) + .rangeEnd(rangeEnd) + .onlyAvailable(onlyAvailable) + .sort(sort) + .from(from) + .size(size) + .build(); + + return eventService.getFreeEvents(freeGetDto, request); + } + + /** + * Получение подробной информации об опубликованном событии по его идентификатору. + * + * @param eventId id события + * @param request Данные HTTP-запроса + * @return {@link EventFullDto} + */ + @GetMapping(value = "/{eventId}") + public EventFullDto getFreeEventById(@PathVariable Long eventId, + HttpServletRequest request) { + return eventService.getFreeEventById(eventId, request); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/controller/rating/RatingController.java b/core/main-service/src/main/java/ru/practicum/ewm/controller/rating/RatingController.java new file mode 100644 index 0000000..0940438 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/controller/rating/RatingController.java @@ -0,0 +1,32 @@ +package ru.practicum.ewm.controller.rating; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.ewm.dto.rating.RatingRequest; +import ru.practicum.ewm.dto.rating.RatingResponse; +import ru.practicum.ewm.service.rating.RatingService; + +@RestController +@RequestMapping("/users/{userId}/events/{eventId}/likes") +@RequiredArgsConstructor +public class RatingController { + + private final RatingService ratingService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public RatingResponse addReaction(@PathVariable Long userId, + @PathVariable Long eventId, + @Valid @RequestBody RatingRequest request) { + return ratingService.addOrUpdateReaction(userId, eventId, request); + } + + @DeleteMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + public void removeReaction(@PathVariable Long userId, + @PathVariable Long eventId) { + ratingService.removeReaction(userId, eventId); + } +} \ No newline at end of file diff --git a/core/main-service/src/main/java/ru/practicum/ewm/controller/user/UserEventController.java b/core/main-service/src/main/java/ru/practicum/ewm/controller/user/UserEventController.java new file mode 100644 index 0000000..005aaad --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/controller/user/UserEventController.java @@ -0,0 +1,139 @@ +package ru.practicum.ewm.controller.user; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.ewm.dto.event.EventFullDto; +import ru.practicum.ewm.dto.event.EventShortDto; +import ru.practicum.ewm.dto.event.NewEventDto; +import ru.practicum.ewm.dto.event.UpdateEventUserRequest; +import ru.practicum.ewm.dto.request.EventRequestStatusUpdateRequest; +import ru.practicum.ewm.dto.request.EventRequestStatusUpdateResult; +import ru.practicum.ewm.dto.request.ParticipationRequestDto; +import ru.practicum.ewm.service.event.EventService; +import ru.practicum.ewm.service.request.RequestService; + +import java.util.List; + +@RestController +@RequestMapping(path = "/users/{userId}/events") +@RequiredArgsConstructor +public class UserEventController { + + private final EventService eventService; + private final RequestService requestService; + + /** + * Получение событий, добавленных текущим пользователем + * + * @param userId id текущего пользователя + * @param from количество элементов, которые нужно пропустить для формирования текущего набора + * Default value : 0 + * @param size количество элементов в наборе + * Default value : 10 + * @return List<{@link EventShortDto}> + */ + @GetMapping + public List findEventsByUserId(@PathVariable @Positive Long userId, + @RequestParam(defaultValue = "0") @PositiveOrZero Integer from, + @RequestParam(defaultValue = "10") @Positive Integer size) { + return eventService.findByUserId(userId, from, size); + } + + /** + * Получение полной информации о событии добавленном текущим пользователем + * + * @param userId id текущего пользователя + * @param eventId id события + * @return {@link EventFullDto} + */ + @GetMapping("/{eventId}") + public EventFullDto findEventById(@PathVariable @Positive Long userId, + @PathVariable @Positive Long eventId) { + return eventService.findEventById(userId, eventId); + } + + /** + * Добавление нового события + *

+ * Обратите внимание: дата и время на которые намечено событие не может быть раньше, + * чем через два часа от текущего момента + * + * @param userId id текущего пользователя + * @param newEventDto данные добавляемого события + * @return {@link EventFullDto} + */ + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public EventFullDto addEvent(@PathVariable @Positive Long userId, + @RequestBody @Valid NewEventDto newEventDto) { + + return eventService.userAddNewEvent(userId, newEventDto); + } + + /** + * Изменение события добавленного текущим пользователем + *

+ * Обратите внимание: + *

+ * - изменить можно только отмененные события или события в состоянии ожидания модерации (Ожидается код ошибки 409) + *

+ * - дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента + * (Ожидается код ошибки 409) + * + * @param userId id текущего пользователя + * @param eventId id редактируемого события + * @param request Данные HTTP-запроса + * @return {@link EventFullDto} + */ + @PatchMapping("/{eventId}") + public EventFullDto patchEvent(@PathVariable @Positive Long userId, + @PathVariable @Positive Long eventId, + @RequestBody @Valid UpdateEventUserRequest request) { + return eventService.patchEvent(userId, eventId, request); + } + + /** + * Получение информации о запросах на участие в событии текущего пользователя + *

+ * В случае, если по заданным фильтрам не найдено ни одной заявки, возвращает пустой список + * + * @param userId id текущего пользователя + * @param eventId id события + * @return List<{@link ParticipationRequestDto}> + */ + @GetMapping("/{eventId}/requests") + public List getRequests(@PathVariable @Positive Long userId, + @PathVariable @Positive Long eventId) { + return requestService.findByEventId(userId, eventId); + } + + /** + * Изменение статуса (подтверждена, отменена) заявок на участие в событии текущего пользователя + *

+ * Обратите внимание: + *

+ * - если для события лимит заявок равен 0 или отключена пре-модерация заявок, то подтверждение заявок не требуется + *

+ * - нельзя подтвердить заявку, если уже достигнут лимит по заявкам на данное событие (Ожидается код ошибки 409) + *

+ * - статус можно изменить только у заявок, находящихся в состоянии ожидания (Ожидается код ошибки 409) + *

+ * - если при подтверждении данной заявки, лимит заявок для события исчерпан, то все неподтверждённые заявки + * необходимо отклонить + * + * @param userId id текущего пользователя + * @param eventId id события текущего пользователя + * @param status Новый статус для заявок на участие в событии текущего пользователя + * @return {@link EventRequestStatusUpdateResult} + */ + @PatchMapping("/{eventId}/requests") + public EventRequestStatusUpdateResult patchRequests(@PathVariable @Positive Long userId, + @PathVariable @Positive Long eventId, + @RequestBody @Valid EventRequestStatusUpdateRequest status) { + return requestService.updateStatusRequest(userId, eventId, status); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/controller/user/UserRequestController.java b/core/main-service/src/main/java/ru/practicum/ewm/controller/user/UserRequestController.java new file mode 100644 index 0000000..d970679 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/controller/user/UserRequestController.java @@ -0,0 +1,72 @@ +package ru.practicum.ewm.controller.user; + +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.ewm.dto.request.ParticipationRequestDto; +import ru.practicum.ewm.service.request.RequestService; + +import java.util.List; + +@Slf4j +@RestController +@RequestMapping(path = "/users/{userId}/requests") +@RequiredArgsConstructor +public class UserRequestController { + private final RequestService service; + + /** + * Получение информации о заявках текущего пользователя на участие в чужих событиях + *

+ * В случае, если по заданным фильтрам не найдено ни одной заявки, возвращает пустой список + * + * @param userId id текущего пользователя + * @return List<{@link ParticipationRequestDto}> + */ + @GetMapping + public List findByRequesterId(@PathVariable @Positive Long userId) { + return service.findByRequesterId(userId); + } + + /** + * Добавление запроса от текущего пользователя на участие в событии + *

+ * Обратите внимание: + *

+ * нельзя добавить повторный запрос (Ожидается код ошибки 409) + *

+ * инициатор события не может добавить запрос на участие в своём событии (Ожидается код ошибки 409) + *

+ * нельзя участвовать в неопубликованном событии (Ожидается код ошибки 409) + *

+ * если у события достигнут лимит запросов на участие - необходимо вернуть ошибку (Ожидается код ошибки 409) + *

+ * если для события отключена пре-модерация запросов на участие, то запрос должен автоматически перейти в + * состояние подтвержденного + * + * @param userId id текущего пользователя + * @param eventId id события + * @return {@link ParticipationRequestDto} + */ + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public ParticipationRequestDto addParticipationRequest(@PathVariable @Positive Long userId, + @RequestParam @Positive Long eventId) { + return service.addParticipationRequest(userId, eventId); + } + + /** + * Отмена своего запроса на участие в событии + * + * @param userId id текущего пользователя + * @param requestId id запроса на участие + * @return {@link ParticipationRequestDto} + */ + @PatchMapping("/{requestId}/cancel") + public ParticipationRequestDto cancelParticipationRequest(@PathVariable @Positive Long userId, + @PathVariable @Positive Long requestId) { + return service.cancelParticipationRequest(userId, requestId); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dao/CategoryRepository.java b/core/main-service/src/main/java/ru/practicum/ewm/dao/CategoryRepository.java new file mode 100644 index 0000000..48dac91 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dao/CategoryRepository.java @@ -0,0 +1,9 @@ +package ru.practicum.ewm.dao; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.ewm.model.Category; + +public interface CategoryRepository extends JpaRepository { + + boolean existsByName(String name); +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dao/CompilationRepository.java b/core/main-service/src/main/java/ru/practicum/ewm/dao/CompilationRepository.java new file mode 100644 index 0000000..0bbb5aa --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dao/CompilationRepository.java @@ -0,0 +1,11 @@ +package ru.practicum.ewm.dao; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.ewm.model.Compilation; + +public interface CompilationRepository extends JpaRepository { + + Page findAllByPinned(boolean pinned, Pageable pageable); +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dao/EventRepository.java b/core/main-service/src/main/java/ru/practicum/ewm/dao/EventRepository.java new file mode 100644 index 0000000..452a0bc --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dao/EventRepository.java @@ -0,0 +1,22 @@ +package ru.practicum.ewm.dao; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import ru.practicum.ewm.model.Event; +import ru.practicum.ewm.model.enums.EventState; + +import java.util.Collection; +import java.util.Optional; + +public interface EventRepository extends JpaRepository, + JpaSpecificationExecutor { + + Collection findByInitiatorId(Long userId, PageRequest pageRequest); + + Optional findByIdAndState(Long eventId, EventState state); + + boolean existsByCategoryId(Long categoryId); + + boolean existsByIdAndState(Long eventId, EventState state); +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dao/RatingRepository.java b/core/main-service/src/main/java/ru/practicum/ewm/dao/RatingRepository.java new file mode 100644 index 0000000..418c9cd --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dao/RatingRepository.java @@ -0,0 +1,16 @@ +package ru.practicum.ewm.dao; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.ewm.model.Rating; +import ru.practicum.ewm.model.enums.Reaction; + +import java.util.Optional; + +public interface RatingRepository extends JpaRepository { + + Optional findByUserIdAndEventId(Long userId, Long eventId); + + boolean existsByUserIdAndEventId(Long userId, Long eventId); + + long countByEventIdAndReaction(Long eventId, Reaction reaction); +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dao/RequestRepository.java b/core/main-service/src/main/java/ru/practicum/ewm/dao/RequestRepository.java new file mode 100644 index 0000000..73c41a5 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dao/RequestRepository.java @@ -0,0 +1,44 @@ +package ru.practicum.ewm.dao; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.ewm.model.ParticipationRequest; +import ru.practicum.ewm.model.enums.ParticipationStatus; +import ru.practicum.ewm.service.request.EventRequestCount; + +import java.util.List; + +public interface RequestRepository extends JpaRepository { + + List findByRequesterId(Long requesterId); + + List findAllByIdIn(List requestIds); + + List findByEventId(Long eventId); + + int countByEventIdAndStatus(Long eventId, ParticipationStatus status); + + @Query(""" + SELECT r.event.id as eventId, + COUNT(r) as count + FROM ParticipationRequest r + WHERE r.event.id IN :eventIds + AND r.status = :status + GROUP BY r.event.id + """) + List countConfirmedRequestsByEventIds(List eventIds, ParticipationStatus status); + + @Modifying + @Transactional + @Query(""" + UPDATE ParticipationRequest pr + SET pr.status = 'REJECTED' + WHERE pr.event.id = :eventId + AND pr.status=:status + """) + int rejectPendingRequests(Long eventId, ParticipationStatus status); + + boolean existsByRequesterIdAndEventId(Long requesterId, Long eventId); +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dao/UserRepository.java b/core/main-service/src/main/java/ru/practicum/ewm/dao/UserRepository.java new file mode 100644 index 0000000..b312916 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dao/UserRepository.java @@ -0,0 +1,14 @@ +package ru.practicum.ewm.dao; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.ewm.model.User; + +import java.util.List; + +public interface UserRepository extends JpaRepository { + + List findAllByIdIn(List ids, Pageable pageable); + + boolean existsByEmail(String email); +} \ No newline at end of file diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/category/CategoryDto.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/category/CategoryDto.java new file mode 100644 index 0000000..3ff7521 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/category/CategoryDto.java @@ -0,0 +1,16 @@ +package ru.practicum.ewm.dto.category; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Builder; + +@Builder +public record CategoryDto( + + Long id, + + @NotBlank + @Size(min = 1, max = 50) + String name +) { +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/category/NewCategoryDto.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/category/NewCategoryDto.java new file mode 100644 index 0000000..2d5e215 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/category/NewCategoryDto.java @@ -0,0 +1,12 @@ +package ru.practicum.ewm.dto.category; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record NewCategoryDto( + + @NotBlank + @Size(min = 1, max = 50) + String name +) { +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/compilation/CompilationDto.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/compilation/CompilationDto.java new file mode 100644 index 0000000..127b712 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/compilation/CompilationDto.java @@ -0,0 +1,20 @@ +package ru.practicum.ewm.dto.compilation; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.ewm.dto.event.EventShortDto; + +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class CompilationDto { + private Long id; + private boolean pinned; + private String title; + private List events; +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/compilation/CompilationSearchFilter.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/compilation/CompilationSearchFilter.java new file mode 100644 index 0000000..68cf761 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/compilation/CompilationSearchFilter.java @@ -0,0 +1,21 @@ +package ru.practicum.ewm.dto.compilation; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CompilationSearchFilter { + + Boolean pinned; + + @Builder.Default + Integer from = 0; + + @Builder.Default + Integer size = 10; +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/compilation/CompilationUpdateDto.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/compilation/CompilationUpdateDto.java new file mode 100644 index 0000000..2f98b0e --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/compilation/CompilationUpdateDto.java @@ -0,0 +1,23 @@ +package ru.practicum.ewm.dto.compilation; + +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class CompilationUpdateDto { + + private Set events; + + private Boolean pinned; + + @Size(min = 1, max = 50) + private String title; +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/compilation/NewCompilationDto.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/compilation/NewCompilationDto.java new file mode 100644 index 0000000..f6ded8b --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/compilation/NewCompilationDto.java @@ -0,0 +1,26 @@ +package ru.practicum.ewm.dto.compilation; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class NewCompilationDto { + + private Set events; + + @Builder.Default + private boolean pinned = false; + + @NotBlank + @Size(min = 1, max = 50) + private String title; +} \ No newline at end of file diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/event/AdminGetDto.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/event/AdminGetDto.java new file mode 100644 index 0000000..c165994 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/event/AdminGetDto.java @@ -0,0 +1,21 @@ +package ru.practicum.ewm.dto.event; + +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import ru.practicum.ewm.model.enums.EventState; + +import java.time.LocalDateTime; +import java.util.List; + +@NotNull +@Builder +public record AdminGetDto( + List users, + List states, + List categories, + LocalDateTime rangeStart, + LocalDateTime rangeEnd, + Integer from, + Integer size +) { +} \ No newline at end of file diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/event/EventFullDto.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/event/EventFullDto.java new file mode 100644 index 0000000..7fc1da1 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/event/EventFullDto.java @@ -0,0 +1,56 @@ +package ru.practicum.ewm.dto.event; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import ru.practicum.ewm.dto.category.CategoryDto; +import ru.practicum.ewm.dto.user.UserShortDto; +import ru.practicum.ewm.model.Location; +import ru.practicum.ewm.model.enums.EventState; + +import java.time.LocalDateTime; + +@Builder +public record EventFullDto( + + Long id, + + @NotBlank + String annotation, + + @NotNull + CategoryDto category, + + Long confirmedRequests, + + LocalDateTime createdOn, + + String description, + + @NotNull + LocalDateTime eventDate, + + @NotNull + UserShortDto initiator, + + @NotNull + Location location, + + boolean paid, + + Integer participantLimit, + + LocalDateTime publishedOn, + + boolean requestModeration, + + EventState state, + + @NotBlank + String title, + + Long views, + + long rate +) { +} \ No newline at end of file diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/event/EventShortDto.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/event/EventShortDto.java new file mode 100644 index 0000000..6f2be95 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/event/EventShortDto.java @@ -0,0 +1,22 @@ +package ru.practicum.ewm.dto.event; + +import lombok.Builder; +import ru.practicum.ewm.dto.category.CategoryDto; +import ru.practicum.ewm.dto.user.UserShortDto; + +import java.time.LocalDateTime; + +@Builder +public record EventShortDto( + long id, + String annotation, + CategoryDto category, + long confirmedRequests, + LocalDateTime eventDate, + UserShortDto initiator, + boolean paid, + String title, + long views, + long rate +) { +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/event/FreeGetDto.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/event/FreeGetDto.java new file mode 100644 index 0000000..0343925 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/event/FreeGetDto.java @@ -0,0 +1,26 @@ +package ru.practicum.ewm.dto.event; + +import jakarta.validation.constraints.NotNull; +import lombok.Builder; + +import java.time.LocalDateTime; +import java.util.List; + +@NotNull +@Builder +public record FreeGetDto( + String text, + List categories, + Boolean paid, + LocalDateTime rangeStart, + LocalDateTime rangeEnd, + Boolean onlyAvailable, + FreeEventSort sort, + Integer from, + Integer size +) { + + public enum FreeEventSort { + EVENT_DATE, VIEWS + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/event/NewEventDto.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/event/NewEventDto.java new file mode 100644 index 0000000..fe9ff80 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/event/NewEventDto.java @@ -0,0 +1,49 @@ +package ru.practicum.ewm.dto.event; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; +import ru.practicum.ewm.model.Location; + +import java.time.LocalDateTime; + +@Data +@Builder() +@Accessors(fluent = true) +@FieldDefaults(makeFinal = true) +public class NewEventDto { + + @NotBlank + @Size(min = 20, max = 2000) + String annotation; + + @NotNull + Long category; + + @NotBlank + @Size(min = 20, max = 7000) + String description; + + @NotNull + LocalDateTime eventDate; + + @NotNull + Location location; + + @Builder.Default + Boolean paid = false; + + @Builder.Default + Integer participantLimit = 0; + + @Builder.Default + Boolean requestModeration = true; + + @NotBlank + @Size(min = 3, max = 120) + String title; +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/event/UpdateEventAdminRequest.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/event/UpdateEventAdminRequest.java new file mode 100644 index 0000000..14c73ec --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/event/UpdateEventAdminRequest.java @@ -0,0 +1,36 @@ +package ru.practicum.ewm.dto.event; + +import jakarta.validation.constraints.Size; +import lombok.Builder; +import ru.practicum.ewm.model.Location; +import ru.practicum.ewm.model.enums.AdminStateAction; + +import java.time.LocalDateTime; + +@Builder +public record UpdateEventAdminRequest( + + @Size(min = 20, max = 2000) + String annotation, + + Long category, + + @Size(min = 20, max = 7000) + String description, + + LocalDateTime eventDate, + + Location location, + + Boolean paid, + + Integer participantLimit, + + Boolean requestModeration, + + AdminStateAction stateAction, + + @Size(min = 3, max = 120) + String title +) { +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/event/UpdateEventUserRequest.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/event/UpdateEventUserRequest.java new file mode 100644 index 0000000..45aa833 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/event/UpdateEventUserRequest.java @@ -0,0 +1,34 @@ +package ru.practicum.ewm.dto.event; + +import jakarta.validation.constraints.Size; +import ru.practicum.ewm.model.Location; +import ru.practicum.ewm.model.enums.UserStateAction; + +import java.time.LocalDateTime; + +public record UpdateEventUserRequest( + + @Size(min = 20, max = 2000) + String annotation, + + Long category, + + @Size(min = 20, max = 7000) + String description, + + LocalDateTime eventDate, + + Location location, + + Boolean paid, + + Integer participantLimit, + + Boolean requestModeration, + + @Size(min = 3, max = 120) + String title, + + UserStateAction stateAction +) { +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/rating/RatingRequest.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/rating/RatingRequest.java new file mode 100644 index 0000000..1a46e1a --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/rating/RatingRequest.java @@ -0,0 +1,18 @@ +package ru.practicum.ewm.dto.rating; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.ewm.model.enums.Reaction; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RatingRequest { + + @NotNull + private Reaction reaction; +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/rating/RatingResponse.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/rating/RatingResponse.java new file mode 100644 index 0000000..3118cfd --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/rating/RatingResponse.java @@ -0,0 +1,18 @@ +package ru.practicum.ewm.dto.rating; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.ewm.model.enums.Reaction; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RatingResponse { + private Long id; + private Long userId; + private Long eventId; + private Reaction reaction; +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/request/EventRequestStatusUpdateRequest.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/request/EventRequestStatusUpdateRequest.java new file mode 100644 index 0000000..f2809f8 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/request/EventRequestStatusUpdateRequest.java @@ -0,0 +1,17 @@ +package ru.practicum.ewm.dto.request; + +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import ru.practicum.ewm.model.enums.RequestUpdateStatus; + +import java.util.List; + +@Builder +public record EventRequestStatusUpdateRequest( + + List requestIds, + + @NotNull + RequestUpdateStatus status +) { +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/request/EventRequestStatusUpdateResult.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/request/EventRequestStatusUpdateResult.java new file mode 100644 index 0000000..42af240 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/request/EventRequestStatusUpdateResult.java @@ -0,0 +1,12 @@ +package ru.practicum.ewm.dto.request; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record EventRequestStatusUpdateResult( + List confirmedRequests, + List rejectedRequests +) { +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/request/ParticipationRequestDto.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/request/ParticipationRequestDto.java new file mode 100644 index 0000000..336e79d --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/request/ParticipationRequestDto.java @@ -0,0 +1,14 @@ +package ru.practicum.ewm.dto.request; + +import lombok.Builder; +import ru.practicum.ewm.model.enums.ParticipationStatus; + +@Builder +public record ParticipationRequestDto( + Long id, + String created, + Long event, + Long requester, + ParticipationStatus status +) { +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/user/NewUserRequest.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/user/NewUserRequest.java new file mode 100644 index 0000000..c1820d7 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/user/NewUserRequest.java @@ -0,0 +1,20 @@ +package ru.practicum.ewm.dto.user; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Builder; + +@Builder +public record NewUserRequest( + + @Size(min = 2, max = 250) + @NotBlank + String name, + + @Email + @Size(min = 6, max = 254) + @NotBlank + String email +) { +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/user/UserDto.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/user/UserDto.java new file mode 100644 index 0000000..80725ca --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/user/UserDto.java @@ -0,0 +1,11 @@ +package ru.practicum.ewm.dto.user; + +import lombok.Builder; + +@Builder +public record UserDto( + Long id, + String name, + String email +) { +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/dto/user/UserShortDto.java b/core/main-service/src/main/java/ru/practicum/ewm/dto/user/UserShortDto.java new file mode 100644 index 0000000..7c8dc62 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/dto/user/UserShortDto.java @@ -0,0 +1,23 @@ +package ru.practicum.ewm.dto.user; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Builder; + +@NotNull +@Builder +public record UserShortDto( + + Long id, + + @NotBlank + String name, + + @Email + @Size(min = 6, max = 255) + @NotBlank + String email +) { +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/mapper/CategoryMapper.java b/core/main-service/src/main/java/ru/practicum/ewm/mapper/CategoryMapper.java new file mode 100644 index 0000000..448c6d4 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/mapper/CategoryMapper.java @@ -0,0 +1,29 @@ +package ru.practicum.ewm.mapper; + +import lombok.experimental.UtilityClass; +import org.springframework.lang.NonNull; +import ru.practicum.ewm.dto.category.CategoryDto; +import ru.practicum.ewm.dto.category.NewCategoryDto; +import ru.practicum.ewm.model.Category; + +@UtilityClass +public class CategoryMapper { + public CategoryDto toDto(@NonNull Category category) { + return CategoryDto.builder() + .id(category.getId()) + .name(category.getName()) + .build(); + } + + public Category toEntity(@NonNull CategoryDto categoryDto) { + return Category.builder() + .name(categoryDto.name()) + .build(); + } + + public Category toEntity(@NonNull NewCategoryDto newCategoryDto) { + return Category.builder() + .name(newCategoryDto.name()) + .build(); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/mapper/CompilationMapper.java b/core/main-service/src/main/java/ru/practicum/ewm/mapper/CompilationMapper.java new file mode 100644 index 0000000..db94788 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/mapper/CompilationMapper.java @@ -0,0 +1,41 @@ +package ru.practicum.ewm.mapper; + +import lombok.experimental.UtilityClass; +import org.springframework.lang.NonNull; +import ru.practicum.ewm.dto.compilation.CompilationDto; +import ru.practicum.ewm.dto.compilation.NewCompilationDto; +import ru.practicum.ewm.model.Compilation; +import ru.practicum.ewm.model.Event; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +@UtilityClass +public class CompilationMapper { + + public CompilationDto toCompilationDto(@NonNull Compilation compilation, + @NonNull Map confirmedRequests, + @NonNull Map views) { + return CompilationDto.builder() + .id(compilation.getId()) + .pinned(compilation.isPinned()) + .title(compilation.getTitle()) + .events(compilation.getEvents().stream() + .map(event -> EventMapper.toEventShortDto( + event, + confirmedRequests.getOrDefault(event.getId(), 0L), + views.getOrDefault(event.getId(), 0L) + )) + .toList()) + .build(); + } + + public Compilation toEntity(@NonNull NewCompilationDto dto, Set events) { + return Compilation.builder() + .title(dto.getTitle()) + .pinned(dto.isPinned()) + .events(events != null ? events : new HashSet<>()) + .build(); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/mapper/EventMapper.java b/core/main-service/src/main/java/ru/practicum/ewm/mapper/EventMapper.java new file mode 100644 index 0000000..de4cbbd --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/mapper/EventMapper.java @@ -0,0 +1,146 @@ +package ru.practicum.ewm.mapper; + +import lombok.experimental.UtilityClass; +import org.springframework.lang.NonNull; +import ru.practicum.ewm.dto.event.*; +import ru.practicum.ewm.model.Category; +import ru.practicum.ewm.model.Event; +import ru.practicum.ewm.model.User; +import ru.practicum.ewm.model.enums.EventState; + +import java.time.LocalDateTime; +import java.util.Optional; + +@UtilityClass +public class EventMapper { + + public EventShortDto toEventShortDto(@NonNull Event event, long confirmedRequests, long views) { + return EventShortDto.builder() + .annotation(event.getAnnotation()) + .category(CategoryMapper.toDto(event.getCategory())) + .confirmedRequests(confirmedRequests) + .eventDate(event.getEventDate()) + .id(event.getId()) + .initiator(UserMapper.toUserShortDto(event.getInitiator())) + .paid(event.isPaid()) + .title(event.getTitle()) + .views(views) + .rate(event.getRate()) + .build(); + } + + public EventFullDto toEventFullDto(@NonNull Event event, long confirmedRequests, long views) { + return EventFullDto.builder() + .id(event.getId()) + .annotation(event.getAnnotation()) + .category(CategoryMapper.toDto(event.getCategory())) + .confirmedRequests(confirmedRequests) + .createdOn(event.getCreatedOn()) + .description(event.getDescription()) + .eventDate(event.getEventDate()) + .initiator(UserMapper.toUserShortDto(event.getInitiator())) + .location(event.getLocation()) + .paid(event.isPaid()) + .participantLimit(event.getParticipantLimit()) + .publishedOn(event.getPublishedOn()) + .requestModeration(event.isRequestModeration()) + .state(event.getState()) + .title(event.getTitle()) + .views(views) + .rate(event.getRate()) + .build(); + } + + public Event toEntity(@NonNull NewEventDto newEventDto, + Category category, + LocalDateTime createdOn, + User initiator, + LocalDateTime publishedOn, + EventState state) { + return Event.builder() + .annotation(newEventDto.annotation()) + .category(category) + .createdOn(createdOn) + .description(newEventDto.description()) + .eventDate(newEventDto.eventDate()) + .initiator(initiator) + .location(newEventDto.location()) + .paid(newEventDto.paid() != null && newEventDto.paid()) + .participantLimit(newEventDto.participantLimit() == null ? 0 : newEventDto.participantLimit()) + .publishedOn(publishedOn) + .requestModeration(newEventDto.requestModeration() == null || newEventDto.requestModeration()) + .state(state) + .title(newEventDto.title()) + .rate(0) + .build(); + } + + public static Event update(@NonNull Event oldEvent, + @NonNull UpdateEventAdminRequest request, + EventState state, + LocalDateTime publishedOn, + @NonNull Optional category) { + + return oldEvent.toBuilder() + .annotation(request.annotation() != null ? + request.annotation() : oldEvent.getAnnotation() + ) + .category(category.orElse(oldEvent.getCategory())) + .description(request.description() != null ? + request.description() : oldEvent.getDescription() + ) + .eventDate(request.eventDate() != null ? + request.eventDate() : oldEvent.getEventDate() + ) + .location(request.location() != null ? + request.location() : oldEvent.getLocation() + ) + .paid(request.paid() != null ? + request.paid() : oldEvent.isPaid() + ) + .participantLimit(request.participantLimit() != null ? + request.participantLimit() : oldEvent.getParticipantLimit() + ) + .requestModeration(request.requestModeration() != null ? + request.requestModeration() : oldEvent.isRequestModeration() + ) + .state(state) + .title(request.title() != null ? + request.title() : oldEvent.getTitle() + ) + .publishedOn(publishedOn) + .build(); + } + + public void merge(Event event, UpdateEventUserRequest dto) { + if (dto == null) return; + + if (dto.annotation() != null) { + event.setAnnotation(dto.annotation()); + } + + if (dto.description() != null) { + event.setDescription(dto.description()); + } + + if (dto.title() != null) { + event.setTitle(dto.title()); + } + + if (dto.eventDate() != null) { + event.setEventDate(dto.eventDate()); + } + + if (dto.paid() != null) { + event.setPaid(dto.paid()); + } + + if (dto.participantLimit() != null) { + event.setParticipantLimit(dto.participantLimit()); + } + + if (dto.requestModeration() != null) { + event.setRequestModeration(dto.requestModeration()); + } + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/mapper/RequestMapper.java b/core/main-service/src/main/java/ru/practicum/ewm/mapper/RequestMapper.java new file mode 100644 index 0000000..dc76c23 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/mapper/RequestMapper.java @@ -0,0 +1,31 @@ +package ru.practicum.ewm.mapper; + +import org.springframework.stereotype.Component; +import ru.practicum.ewm.dto.request.ParticipationRequestDto; +import ru.practicum.ewm.model.ParticipationRequest; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + + +@Component +public class RequestMapper { + + public static ParticipationRequestDto toParticipationRequestDto(ParticipationRequest participationRequest) { + if (participationRequest == null) return null; + return ParticipationRequestDto.builder() + .event(participationRequest.getEvent().getId()) + .requester(participationRequest.getRequester().getId()) + .status(participationRequest.getStatus()) + .created(formatDateTime(participationRequest.getCreated())) + .id(participationRequest.getId()) + .build(); + } + + private static String formatDateTime(LocalDateTime dateTime) { + if (dateTime == null) return null; + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS"); + return dateTime.format(formatter); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/mapper/StateMapper.java b/core/main-service/src/main/java/ru/practicum/ewm/mapper/StateMapper.java new file mode 100644 index 0000000..3acbea8 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/mapper/StateMapper.java @@ -0,0 +1,25 @@ +package ru.practicum.ewm.mapper; + +import org.springframework.stereotype.Component; +import ru.practicum.ewm.model.enums.EventState; +import ru.practicum.ewm.model.enums.UserStateAction; + +@Component +public class StateMapper { + + public static EventState mapUserEventAction(UserStateAction action) { + return switch (action) { + case CANCEL_REVIEW -> EventState.CANCELED; + case SEND_TO_REVIEW -> EventState.PENDING; + case null, default -> null; + }; + } + + public static EventState mapAdminEventAction(UserStateAction action) { + return switch (action) { + case PUBLISH_EVENT -> EventState.PUBLISHED; + case REJECT_EVENT -> EventState.CANCELED; + case null, default -> null; + }; + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/mapper/UserMapper.java b/core/main-service/src/main/java/ru/practicum/ewm/mapper/UserMapper.java new file mode 100644 index 0000000..cfa070f --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/mapper/UserMapper.java @@ -0,0 +1,35 @@ +package ru.practicum.ewm.mapper; + +import lombok.experimental.UtilityClass; +import org.springframework.lang.NonNull; +import ru.practicum.ewm.dto.user.NewUserRequest; +import ru.practicum.ewm.dto.user.UserDto; +import ru.practicum.ewm.dto.user.UserShortDto; +import ru.practicum.ewm.model.User; + +@UtilityClass +public class UserMapper { + + public UserShortDto toUserShortDto(@NonNull User user) { + return UserShortDto.builder() + .id(user.getId()) + .name(user.getName()) + .email(user.getEmail()) + .build(); + } + + public UserDto toUserDto(@NonNull User user) { + return UserDto.builder() + .id(user.getId()) + .name(user.getName()) + .email(user.getEmail()) + .build(); + } + + public User toEntity(@NonNull NewUserRequest newUserRequest) { + return User.builder() + .name(newUserRequest.name()) + .email(newUserRequest.email()) + .build(); + } +} \ No newline at end of file diff --git a/core/main-service/src/main/java/ru/practicum/ewm/model/Category.java b/core/main-service/src/main/java/ru/practicum/ewm/model/Category.java new file mode 100644 index 0000000..d21838c --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/model/Category.java @@ -0,0 +1,22 @@ +package ru.practicum.ewm.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; +import ru.practicum.ewm.util.entity.BaseEntity; + +@Getter +@Setter +@Entity +@Table(name = "categories") +@SuperBuilder +@NoArgsConstructor +public class Category extends BaseEntity { + + @Column(nullable = false, length = 50, unique = true) + private String name; +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/model/Compilation.java b/core/main-service/src/main/java/ru/practicum/ewm/model/Compilation.java new file mode 100644 index 0000000..5d02b70 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/model/Compilation.java @@ -0,0 +1,33 @@ +package ru.practicum.ewm.model; + +import jakarta.persistence.*; +import lombok.*; +import lombok.experimental.FieldDefaults; +import lombok.experimental.SuperBuilder; +import ru.practicum.ewm.util.entity.BaseEntity; + +import java.util.HashSet; +import java.util.Set; + +@Getter +@Setter +@Entity +@Table(name = "compilations") +@SuperBuilder +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class Compilation extends BaseEntity { + + @Column(nullable = false, length = 50) + String title; + + @ManyToMany + @JoinTable(name = "compilation_events", + joinColumns = @JoinColumn(name = "compilations_id"), + inverseJoinColumns = @JoinColumn(name = "events_id")) + @Builder.Default + Set events = new HashSet<>(); + + @Column(nullable = false) + boolean pinned; +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/model/Event.java b/core/main-service/src/main/java/ru/practicum/ewm/model/Event.java new file mode 100644 index 0000000..32f8cf0 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/model/Event.java @@ -0,0 +1,92 @@ +package ru.practicum.ewm.model; + +import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.FieldDefaults; +import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.annotations.Type; +import org.hibernate.type.SqlTypes; +import ru.practicum.ewm.model.enums.EventState; +import ru.practicum.ewm.util.entity.BaseEntity; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Setter +@Entity +@Table(name = "events") +@SuperBuilder(toBuilder = true) +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class Event extends BaseEntity { + + /// Краткое описание + @Column(nullable = false, length = 2000) + String annotation; + + /// Категория + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "category_id") + Category category; + + /// Дата и время создания события (в формате "yyyy-MM-dd HH:mm:ss") + @Column(nullable = false) + LocalDateTime createdOn; + + /// Полное описание события + @Column(nullable = false, length = 7000) + String description; + + /// Дата и время на которые намечено событие (в формате "yyyy-MM-dd HH:mm:ss") + @Column(nullable = false) + LocalDateTime eventDate; + + /// Пользователь инициатор события + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "initiator_id") + User initiator; + + /// Широта и долгота места проведения события + @Type(JsonBinaryType.class) + @JdbcTypeCode(SqlTypes.JSON) + @Column(columnDefinition = "jsonb") + Location location; + + /// Нужно ли оплачивать участие + @Column(nullable = false) + boolean paid; + + /// Ограничение на количество участников. Значение 0 - означает отсутствие ограничения + @Column(nullable = false) + int participantLimit; + + /// Дата и время публикации события (в формате "yyyy-MM-dd HH:mm:ss") + LocalDateTime publishedOn; + + /// Нужна ли пре-модерация заявок на участие + @Column(nullable = false) + boolean requestModeration; + + /// Список состояний жизненного цикла события ( PENDING, PUBLISHED, CANCELED ) + @Column(nullable = false) + @Enumerated(EnumType.STRING) + EventState state; + + /// Заголовок + @Column(nullable = false, length = 120) + String title; + + /// В каких компиляциях состоит событие + @ManyToMany(mappedBy = "events") + List compilations; + + /// Рейтинг события, вычисляемый как количество лайков и дизлайков + @Column(nullable = false) + private long rate; +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/model/Location.java b/core/main-service/src/main/java/ru/practicum/ewm/model/Location.java new file mode 100644 index 0000000..fb54d09 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/model/Location.java @@ -0,0 +1,13 @@ +package ru.practicum.ewm.model; + +import lombok.Builder; +import lombok.Data; + +import java.io.Serializable; + +@Data +@Builder +public class Location implements Serializable { + Float lat; + Float lon; +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/model/ParticipationRequest.java b/core/main-service/src/main/java/ru/practicum/ewm/model/ParticipationRequest.java new file mode 100644 index 0000000..67bf763 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/model/ParticipationRequest.java @@ -0,0 +1,37 @@ +package ru.practicum.ewm.model; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.FieldDefaults; +import lombok.experimental.SuperBuilder; +import ru.practicum.ewm.model.enums.ParticipationStatus; +import ru.practicum.ewm.util.entity.BaseEntity; + +import java.time.LocalDateTime; + +@Getter +@Setter +@Entity +@Table(name = "requests") +@SuperBuilder +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class ParticipationRequest extends BaseEntity { + + LocalDateTime created; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "event_id", nullable = false) + Event event; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "requester_id", nullable = false) + User requester; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + ParticipationStatus status; +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/model/Rating.java b/core/main-service/src/main/java/ru/practicum/ewm/model/Rating.java new file mode 100644 index 0000000..f10552c --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/model/Rating.java @@ -0,0 +1,35 @@ +package ru.practicum.ewm.model; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.FieldDefaults; +import lombok.experimental.SuperBuilder; +import ru.practicum.ewm.model.enums.Reaction; +import ru.practicum.ewm.util.entity.BaseEntity; + +@Getter +@Setter +@Entity +@Table(name = "ratings", uniqueConstraints = { + @UniqueConstraint(columnNames = {"user_id", "event_id"}) +}) +@SuperBuilder +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class Rating extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "event_id", nullable = false) + private Event event; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Reaction reaction; +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/model/User.java b/core/main-service/src/main/java/ru/practicum/ewm/model/User.java new file mode 100644 index 0000000..a2815eb --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/model/User.java @@ -0,0 +1,28 @@ +package ru.practicum.ewm.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.FieldDefaults; +import lombok.experimental.SuperBuilder; +import ru.practicum.ewm.util.entity.BaseEntity; + +@Getter +@Setter +@Entity +@Table(name = "users") +@SuperBuilder +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class User extends BaseEntity { + + @Column(nullable = false) + String name; + + @Column(nullable = false, unique = true) + String email; +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/model/enums/AdminStateAction.java b/core/main-service/src/main/java/ru/practicum/ewm/model/enums/AdminStateAction.java new file mode 100644 index 0000000..88f2fa6 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/model/enums/AdminStateAction.java @@ -0,0 +1,5 @@ +package ru.practicum.ewm.model.enums; + +public enum AdminStateAction { + PUBLISH_EVENT, REJECT_EVENT +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/model/enums/EventState.java b/core/main-service/src/main/java/ru/practicum/ewm/model/enums/EventState.java new file mode 100644 index 0000000..73987d3 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/model/enums/EventState.java @@ -0,0 +1,5 @@ +package ru.practicum.ewm.model.enums; + +public enum EventState { + PENDING, PUBLISHED, CANCELED +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/model/enums/ParticipationStatus.java b/core/main-service/src/main/java/ru/practicum/ewm/model/enums/ParticipationStatus.java new file mode 100644 index 0000000..b16afe2 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/model/enums/ParticipationStatus.java @@ -0,0 +1,5 @@ +package ru.practicum.ewm.model.enums; + +public enum ParticipationStatus { + PENDING, CONFIRMED, REJECTED, CANCELED +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/model/enums/Reaction.java b/core/main-service/src/main/java/ru/practicum/ewm/model/enums/Reaction.java new file mode 100644 index 0000000..d379a2c --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/model/enums/Reaction.java @@ -0,0 +1,5 @@ +package ru.practicum.ewm.model.enums; + +public enum Reaction { + LIKE, DISLIKE +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/model/enums/RequestUpdateStatus.java b/core/main-service/src/main/java/ru/practicum/ewm/model/enums/RequestUpdateStatus.java new file mode 100644 index 0000000..37d71e0 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/model/enums/RequestUpdateStatus.java @@ -0,0 +1,5 @@ +package ru.practicum.ewm.model.enums; + +public enum RequestUpdateStatus { + CONFIRMED, REJECTED +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/model/enums/UserStateAction.java b/core/main-service/src/main/java/ru/practicum/ewm/model/enums/UserStateAction.java new file mode 100644 index 0000000..ce9624b --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/model/enums/UserStateAction.java @@ -0,0 +1,5 @@ +package ru.practicum.ewm.model.enums; + +public enum UserStateAction { + SEND_TO_REVIEW, CANCEL_REVIEW, PUBLISH_EVENT, REJECT_EVENT +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/service/category/CategoryService.java b/core/main-service/src/main/java/ru/practicum/ewm/service/category/CategoryService.java new file mode 100644 index 0000000..c1843cf --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/service/category/CategoryService.java @@ -0,0 +1,23 @@ +package ru.practicum.ewm.service.category; + +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.ewm.dto.category.CategoryDto; +import ru.practicum.ewm.dto.category.NewCategoryDto; + +import java.util.List; + +public interface CategoryService { + + @Transactional + CategoryDto adminAddNewCategory(NewCategoryDto newCategoryDto); + + List findAll(Integer from, Integer size); + + CategoryDto findById(Long catId); + + @Transactional + CategoryDto updateCategory(Long catId, CategoryDto categoryDto); + + @Transactional + void deleteCategory(Long catId); +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/service/category/CategoryServiceImpl.java b/core/main-service/src/main/java/ru/practicum/ewm/service/category/CategoryServiceImpl.java new file mode 100644 index 0000000..28e435e --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/service/category/CategoryServiceImpl.java @@ -0,0 +1,87 @@ +package ru.practicum.ewm.service.category; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.experimental.FieldDefaults; +import org.springframework.data.domain.PageRequest; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.ewm.dao.CategoryRepository; +import ru.practicum.ewm.dao.EventRepository; +import ru.practicum.ewm.dto.category.CategoryDto; +import ru.practicum.ewm.dto.category.NewCategoryDto; +import ru.practicum.ewm.mapper.CategoryMapper; +import ru.practicum.ewm.model.Category; +import ru.practicum.ewm.util.error.exception.ConflictException; +import ru.practicum.ewm.util.error.exception.NotFoundException; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +public class CategoryServiceImpl implements CategoryService { + + CategoryRepository categoryRepository; + EventRepository eventRepository; + + @Override + public CategoryDto adminAddNewCategory(@NonNull NewCategoryDto newCategoryDto) { + if (categoryRepository.existsByName(newCategoryDto.name())) { + throw new ConflictException( + "Категория с именем '" + newCategoryDto.name() + "' уже существует"); + } + Category category = CategoryMapper.toEntity(newCategoryDto); + return CategoryMapper.toDto(categoryRepository.save(category)); + } + + @Override + public List findAll(Integer from, Integer size) { + int page = from / size; + + return categoryRepository.findAll(PageRequest.of(page, size)).stream() + .map(CategoryMapper::toDto) + .collect(Collectors.toList()); + } + + @Override + public CategoryDto findById(Long catId) { + Category category = getCategoryById(catId); + + return CategoryMapper.toDto(category); + } + + @Override + @Transactional + public CategoryDto updateCategory(Long catId, @NonNull CategoryDto categoryDto) { + Category category = getCategoryById(catId); + if (!category.getName().equals(categoryDto.name()) && + categoryRepository.existsByName(categoryDto.name())) { + throw new ConflictException( + "Категория с именем '" + categoryDto.name() + "' уже существует"); + } + + category.setName(categoryDto.name()); + return CategoryMapper.toDto(categoryRepository.save(category)); + } + + @Override + @Transactional + public void deleteCategory(Long catId) { + Category category = getCategoryById(catId); + if (eventRepository.existsByCategoryId(catId)) { + throw new ConflictException("The category is not empty"); + } + + categoryRepository.delete(category); + } + + @NonNull + private Category getCategoryById(long categoryId) { + return categoryRepository.findById(categoryId).orElseThrow( + () -> new NotFoundException("Категория с id=" + categoryId + " не найдена") + ); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/service/compilation/CompilationService.java b/core/main-service/src/main/java/ru/practicum/ewm/service/compilation/CompilationService.java new file mode 100644 index 0000000..8af94ca --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/service/compilation/CompilationService.java @@ -0,0 +1,22 @@ +package ru.practicum.ewm.service.compilation; + +import jakarta.servlet.http.HttpServletRequest; +import ru.practicum.ewm.dto.compilation.CompilationDto; +import ru.practicum.ewm.dto.compilation.CompilationSearchFilter; +import ru.practicum.ewm.dto.compilation.CompilationUpdateDto; +import ru.practicum.ewm.dto.compilation.NewCompilationDto; + +import java.util.List; + +public interface CompilationService { + + CompilationDto getById(Long compilationId, HttpServletRequest request); + + void delById(Long compilationId); + + CompilationDto addCompilation(NewCompilationDto compilation); + + CompilationDto updateCompilation(Long compilationId, CompilationUpdateDto compilation); + + List getByFilter(CompilationSearchFilter filter, HttpServletRequest request); +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/service/compilation/CompilationServiceImpl.java b/core/main-service/src/main/java/ru/practicum/ewm/service/compilation/CompilationServiceImpl.java new file mode 100644 index 0000000..90f4cff --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/service/compilation/CompilationServiceImpl.java @@ -0,0 +1,206 @@ +package ru.practicum.ewm.service.compilation; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.experimental.FieldDefaults; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.ewm.dao.CompilationRepository; +import ru.practicum.ewm.dao.EventRepository; +import ru.practicum.ewm.dao.RequestRepository; +import ru.practicum.ewm.dto.compilation.CompilationDto; +import ru.practicum.ewm.dto.compilation.CompilationSearchFilter; +import ru.practicum.ewm.dto.compilation.CompilationUpdateDto; +import ru.practicum.ewm.dto.compilation.NewCompilationDto; +import ru.practicum.ewm.mapper.CompilationMapper; +import ru.practicum.ewm.model.Compilation; +import ru.practicum.ewm.model.Event; +import ru.practicum.ewm.model.enums.ParticipationStatus; +import ru.practicum.ewm.service.request.EventRequestCount; +import ru.practicum.ewm.util.error.exception.NotFoundException; +import ru.practicum.ewm.util.statistic.StatRepository; +import ru.practicum.stat.dto.ViewStatsDto; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +public class CompilationServiceImpl implements CompilationService { + + CompilationRepository compilationRepository; + EventRepository eventRepository; + StatRepository statRepository; + RequestRepository requestRepository; + + @Override + public CompilationDto getById(Long compilationId, HttpServletRequest request) { + statRepository.sendHitRequest(request); + + Compilation compilation = getCompilationById(compilationId); + + return CompilationMapper.toCompilationDto( + compilation, + getConfirmedRequests(List.of(compilation)), + getViews(List.of(compilation)) + ); + } + + @Override + @Transactional + public void delById(Long compilationId) { + getCompilationById(compilationId); + compilationRepository.deleteById(compilationId); + } + + @Override + @Transactional + public CompilationDto addCompilation(@NonNull NewCompilationDto compilationDto) { + Set events = new HashSet<>(); + + if (compilationDto.getEvents() != null && !compilationDto.getEvents().isEmpty()) { + events = new HashSet<>(eventRepository.findAllById(compilationDto.getEvents())); + + if (events.size() < compilationDto.getEvents().size()) { + throw new NotFoundException("Одно или несколько событий не найдены"); + } + } + + Compilation compilation = CompilationMapper.toEntity(compilationDto, events); + Compilation savedCompilation = compilationRepository.save(compilation); + + Map confirmedRequests = new HashMap<>(); + savedCompilation.getEvents().forEach(event -> confirmedRequests.put(event.getId(), 0L)); + + Map views = new HashMap<>(); + savedCompilation.getEvents().forEach(event -> views.put(event.getId(), 0L)); + + return CompilationMapper.toCompilationDto( + savedCompilation, + confirmedRequests, + views + ); + } + + @Override + @Transactional + public CompilationDto updateCompilation(Long compilationId, @NonNull CompilationUpdateDto compilationUpdateDto) { + Compilation compilationInDb = getCompilationById(compilationId); + + if (compilationUpdateDto.getEvents() != null) { + List eventsUpdate = new ArrayList<>(); + + if (!compilationUpdateDto.getEvents().isEmpty()) { + eventsUpdate = eventRepository.findAllById(compilationUpdateDto.getEvents()); + if (eventsUpdate.size() < compilationUpdateDto.getEvents().size()) { + throw new NotFoundException("Одно или несколько событий не найдены"); + } + } + compilationInDb.setEvents(new HashSet<>(eventsUpdate)); + + } + + if (compilationUpdateDto.getTitle() != null && !compilationUpdateDto.getTitle().isBlank()) { + compilationInDb.setTitle(compilationUpdateDto.getTitle()); + } + + if (compilationUpdateDto.getPinned() != null) { + compilationInDb.setPinned(compilationUpdateDto.getPinned()); + } + + return CompilationMapper.toCompilationDto( + compilationInDb, getConfirmedRequests(List.of(compilationInDb)), getViews(List.of(compilationInDb))); + } + + @Override + public List getByFilter(@NonNull CompilationSearchFilter filter, HttpServletRequest request) { + Pageable pageable = PageRequest.of(filter.getFrom() / filter.getSize(), filter.getSize()); + Page compilationsPage; + + if (filter.getPinned() != null) { + compilationsPage = compilationRepository.findAllByPinned(filter.getPinned(), pageable); + } else { + compilationsPage = compilationRepository.findAll(pageable); + } + + List compilations = compilationsPage.getContent(); + + if (compilations.isEmpty()) { + return Collections.emptyList(); + } + + Map allConfirmedRequests = getConfirmedRequests(compilations); + Map allViews = getViews(compilations); + + // Теперь маппим, просто подставляя готовые Map + return compilations.stream() + .map(compilation -> CompilationMapper.toCompilationDto( + compilation, + allConfirmedRequests, + allViews + )) + .toList(); + } + + @NonNull + private Compilation getCompilationById(long compilationId) { + return compilationRepository.findById(compilationId).orElseThrow( + () -> new NotFoundException("Подборка с id=" + compilationId + " не найдена") + ); + } + + /// Map + @NonNull + private Map getConfirmedRequests(Collection compilations) { + List events = compilations.stream() + .flatMap(c -> c.getEvents().stream()) + .toList(); + + if (events.isEmpty()) return Collections.emptyMap(); + + List eventRequestCountList = requestRepository.countConfirmedRequestsByEventIds( + events.stream().map(Event::getId).toList(), + ParticipationStatus.CONFIRMED); + + return eventRequestCountList.stream() + .collect(Collectors.toMap( + EventRequestCount::getEventId, + EventRequestCount::getCount, + (existing, replacement) -> existing + )); + } + + /// Map + @NonNull + private Map getViews(Collection compilations) { + // Все уникальные события + List allEvents = compilations.stream() + .flatMap(c -> c.getEvents().stream()) + .distinct() + .toList(); + + if (allEvents.isEmpty()) return Collections.emptyMap(); + + // Все URIs + List uris = allEvents.stream() + .map(e -> "/events/" + e.getId()) + .toList(); + + List stats = statRepository.getStat(uris, false); + + // Map + return stats.stream() + .collect(Collectors.toMap( + s -> Long.parseLong(s.getUri().replace("/events/", "")), + ViewStatsDto::getHits, + (a, b) -> a + )); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/service/event/EventService.java b/core/main-service/src/main/java/ru/practicum/ewm/service/event/EventService.java new file mode 100644 index 0000000..af8549f --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/service/event/EventService.java @@ -0,0 +1,39 @@ +package ru.practicum.ewm.service.event; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.ewm.dto.event.*; + +import java.util.List; + +@Transactional +public interface EventService { + + /// Получение событий незарегистрированным пользователем с возможностью фильтрации + List getFreeEvents(FreeGetDto freeGetDto, HttpServletRequest request); + + /** + * Получение незарегистрированным пользователем подробной информации об опубликованном событии + * по его идентификатору + */ + EventFullDto getFreeEventById(Long eventId, HttpServletRequest request); + + /// Добавление нового события зарегистрированным пользователем + EventFullDto userAddNewEvent(Long userId, NewEventDto newEventDto); + + /// Поиск событий администратором + @Transactional(readOnly = true) + List adminGetEvents(AdminGetDto adminGetDto); + + /// Редактирование администратором данных события и его статуса (отклонение/публикация) + EventFullDto adminUpdateEvent(Long eventId, UpdateEventAdminRequest request); + + /// Получение событий, добавленных текущим пользователем + List findByUserId(Long userId, Integer from, Integer size); + + /// Получение полной информации о событии добавленном текущим пользователем + EventFullDto findEventById(Long userId, Long eventId); + + /// Изменение события добавленного текущим пользователем + EventFullDto patchEvent(Long userId, Long eventId, UpdateEventUserRequest request); +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/service/event/EventServiceImpl.java b/core/main-service/src/main/java/ru/practicum/ewm/service/event/EventServiceImpl.java new file mode 100644 index 0000000..038df7a --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/service/event/EventServiceImpl.java @@ -0,0 +1,465 @@ +package ru.practicum.ewm.service.event; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ValidationException; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.experimental.FieldDefaults; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import ru.practicum.ewm.dao.CategoryRepository; +import ru.practicum.ewm.dao.EventRepository; +import ru.practicum.ewm.dao.RequestRepository; +import ru.practicum.ewm.dao.UserRepository; +import ru.practicum.ewm.dto.event.*; +import ru.practicum.ewm.mapper.EventMapper; +import ru.practicum.ewm.mapper.StateMapper; +import ru.practicum.ewm.model.Category; +import ru.practicum.ewm.model.Event; +import ru.practicum.ewm.model.User; +import ru.practicum.ewm.model.enums.AdminStateAction; +import ru.practicum.ewm.model.enums.EventState; +import ru.practicum.ewm.model.enums.ParticipationStatus; +import ru.practicum.ewm.model.enums.UserStateAction; +import ru.practicum.ewm.service.request.EventRequestCount; +import ru.practicum.ewm.util.error.exception.ConflictException; +import ru.practicum.ewm.util.error.exception.NotFoundException; +import ru.practicum.ewm.util.specification.EventSpecifications; +import ru.practicum.ewm.util.specification.SpecBuilder; +import ru.practicum.ewm.util.statistic.StatRepository; +import ru.practicum.stat.dto.ViewStatsDto; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.*; + +@Slf4j +@Service +@RequiredArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +public class EventServiceImpl implements EventService { + + StatRepository statRepository; + EventRepository eventRepository; + UserRepository userRepository; + CategoryRepository categoryRepository; + RequestRepository requestRepository; + + @Override + public List getFreeEvents(@NonNull FreeGetDto dto, HttpServletRequest request) { + + if (dto.rangeStart() != null && dto.rangeEnd() != null) { + if (dto.rangeEnd().isBefore(dto.rangeStart())) { + throw new ValidationException("Окончание события не может быть раньше начала"); + } + } + + statRepository.sendHitRequest(request); + + SpecBuilder builder = SpecBuilder.builder() + .and(EventSpecifications.isPublished()) + .andIf(dto.text() != null && !dto.text().isBlank(), + () -> EventSpecifications.textContains(dto.text())) + .andIf(dto.categories() != null && !dto.categories().isEmpty(), + () -> EventSpecifications.hasCategories(dto.categories())) + .andIf(dto.paid() != null, + () -> EventSpecifications.isPaid(dto.paid())) + .andIf(Boolean.TRUE.equals(dto.onlyAvailable()), + () -> EventSpecifications.onlyAvailable(true)); + + boolean hasStart = dto.rangeStart() != null; + boolean hasEnd = dto.rangeEnd() != null; + + if (!hasStart && !hasEnd) { + builder.and(EventSpecifications.eventDateAfterNow(LocalDateTime.now())); + } else { + builder + .andIf(hasStart, + () -> EventSpecifications.dateAfter(dto.rangeStart())) + .andIf(hasEnd, + () -> EventSpecifications.dateBefore(dto.rangeEnd())); + } + + Specification spec = builder.build(); + + Sort sort = Sort.unsorted(); + + if (dto.sort() != null) { + switch (dto.sort()) { + case EVENT_DATE -> sort = Sort.by("eventDate").ascending(); + case VIEWS -> sort = Sort.by("views").descending(); + } + } + + Pageable pageable = PageRequest.of( + dto.from() / dto.size(), + dto.size(), + sort + ); + + List events = eventRepository.findAll(spec, pageable).getContent(); + if (events.isEmpty()) { + return Collections.emptyList(); + } + + List eventRequestCountList = requestRepository.countConfirmedRequestsByEventIds( + events.stream().map(Event::getId).toList(), ParticipationStatus.CONFIRMED); + + Map requestCountMap = new HashMap<>(); + if (!eventRequestCountList.isEmpty()) { + eventRequestCountList.forEach(eventRequestCount -> { + requestCountMap.put(eventRequestCount.getEventId(), eventRequestCount.getCount()); + }); + } + + List uris = events.stream().map(event -> "/events/" + event.getId()).toList(); + List stats = statRepository.getStat( + uris, + dto.rangeStart(), + dto.rangeEnd(), + false); + + return events.stream() + .map(event -> EventMapper.toEventShortDto( + event, + requestCountMap.get(event.getId()) == null ? 0L : requestCountMap.get(event.getId()), + stats.size() + ) + ).toList(); + } + + @Override + public EventFullDto getFreeEventById(Long eventId, HttpServletRequest request) { + if (!eventRepository.existsByIdAndState(eventId, EventState.PUBLISHED)) { + throw new NotFoundException("Событие с id=" + eventId + " не существует или не опубликовано."); + } + statRepository.sendHitRequest(request); + Event event = getEventById(eventId); + + long views = statRepository.getStat(List.of(request.getRequestURI()), true).getFirst().getHits(); + + long confirmedRequests = getConfirmedRequests(eventId); + + return EventMapper.toEventFullDto(event, confirmedRequests, views); + } + + @Override + public EventFullDto userAddNewEvent(Long userId, @NonNull NewEventDto newEventDto) { + if (newEventDto.participantLimit() != null && newEventDto.participantLimit() < 0) { + throw new ValidationException("Ограничение на количество участников должно быть положительным числом"); + } + + if (newEventDto.eventDate() != null && newEventDto.eventDate().isBefore(LocalDateTime.now().plusHours(2))) { + throw new ValidationException("Начало события не может быть раньше, " + + "чем через два часа от текущего момента"); + } + + User initiator = getUserById(userId); + Category category = getCategoryById(newEventDto.category()); + + Event event = EventMapper.toEntity( + newEventDto, + category, + LocalDateTime.now(), + initiator, + null, + EventState.PENDING + ); + + return EventMapper.toEventFullDto(eventRepository.save(event), 0L, 0L); + } + + @Override + public List adminGetEvents(@NonNull AdminGetDto dto) { + Specification spec = SpecBuilder.builder() + .andIf(dto.users() != null && !dto.users().isEmpty(), + () -> EventSpecifications.hasUsers(dto.users())) + .andIf(dto.states() != null && !dto.states().isEmpty(), + () -> EventSpecifications.hasStates(dto.states())) + .andIf(dto.categories() != null && !dto.categories().isEmpty(), + () -> EventSpecifications.hasCategories(dto.categories())) + .andIf(dto.rangeStart() != null, + () -> EventSpecifications.dateAfter(dto.rangeStart())) + .andIf(dto.rangeEnd() != null, + () -> EventSpecifications.dateBefore(dto.rangeEnd())) + .build(); + + Pageable pageable = PageRequest.of( + dto.from() / dto.size(), + dto.size() + ); + + List events = eventRepository.findAll(spec, pageable).getContent(); + if (events.isEmpty()) { + return Collections.emptyList(); + } + + List eventRequestCountList = requestRepository.countConfirmedRequestsByEventIds( + events.stream().map(Event::getId).toList(), ParticipationStatus.CONFIRMED); + + Map requestCountMap = new HashMap<>(); + if (!eventRequestCountList.isEmpty()) { + eventRequestCountList.forEach(eventRequestCount -> + requestCountMap.put(eventRequestCount.getEventId(), eventRequestCount.getCount()) + ); + } + + List uris = events.stream().map(event -> "/events/" + event.getId()).toList(); + List stats = statRepository.getStat( + uris, + dto.rangeStart(), + dto.rangeEnd(), + false); + + return events.stream() + .map(event -> EventMapper.toEventFullDto( + event, + getConfirmedRequests(requestCountMap, event.getId()), + getHits(stats, event.getId()) + ) + ).toList(); + } + + @Override + public EventFullDto adminUpdateEvent(Long eventId, @NonNull UpdateEventAdminRequest request) { + Event oldEvent = getEventById(eventId); + Event newEvent; + + if (request.eventDate() != null && request.eventDate().isBefore(LocalDateTime.now().plusHours(2))) { + throw new ValidationException("Дата события должна быть не может быть раньше, " + + "чем через два часа от текущего момента"); + } + + if (request.stateAction() == null) { + newEvent = EventMapper.update( + oldEvent, + request, + oldEvent.getState(), + oldEvent.getPublishedOn(), + request.category() == null ? + Optional.empty() : + Optional.of(getCategoryById(request.category())) + ); + } else { + + if (request.stateAction().equals(AdminStateAction.PUBLISH_EVENT) && + oldEvent.getState().equals(EventState.PUBLISHED)) { + throw new ConflictException("Событие с id=" + oldEvent.getId() + " уже опубликовано"); + } + + if (request.stateAction().equals(AdminStateAction.PUBLISH_EVENT) && + oldEvent.getState().equals(EventState.CANCELED)) { + throw new ConflictException("Публикация события с id=" + oldEvent.getId() + + " уже отменена пользователем"); + } + + if (request.stateAction().equals(AdminStateAction.REJECT_EVENT) && + oldEvent.getState().equals(EventState.PUBLISHED)) { + throw new ConflictException("Событие с id=" + oldEvent.getId() + + " уже опубликовано, отмена не возможна"); + } + + newEvent = EventMapper.update( + oldEvent, + request, + request.stateAction().equals(AdminStateAction.REJECT_EVENT) ? + EventState.CANCELED : EventState.PUBLISHED, + request.stateAction().equals(AdminStateAction.REJECT_EVENT) ? + null : LocalDateTime.now(), + request.category() == null ? + Optional.empty() : + Optional.of(getCategoryById(request.category())) + ); + } + + return EventMapper.toEventFullDto( + eventRepository.save(newEvent), + getConfirmedRequests(oldEvent.getId()), + getHits(oldEvent.getId()) + ); + } + + @Override + public List findByUserId(Long userId, Integer from, Integer size) { + checkUser(userId); + + PageRequest pageRequest = PageRequest.of(from / size, size); + Collection events = eventRepository.findByInitiatorId(userId, pageRequest); + if (events.isEmpty()) { + return Collections.emptyList(); + } + + List eventRequestCountList = requestRepository.countConfirmedRequestsByEventIds( + events.stream().map(Event::getId).toList(), ParticipationStatus.CONFIRMED); + + Map requestCountMap = new HashMap<>(); + if (!eventRequestCountList.isEmpty()) { + eventRequestCountList.forEach(eventRequestCount -> + requestCountMap.put(eventRequestCount.getEventId(), eventRequestCount.getCount()) + ); + } + + List uris = events.stream().map(event -> "/events/" + event.getId()).toList(); + List stats = statRepository.getStat(uris, true); + + return events.stream() + .map(event -> EventMapper.toEventShortDto( + event, + getConfirmedRequests(requestCountMap, event.getId()), + getHits(stats, event.getId()) + ) + ).toList(); + } + + @Override + public EventFullDto findEventById(Long userId, Long eventId) { + checkUser(userId); + + Event event = getEventById(eventId); + + if (!event.getInitiator().getId().equals(userId)) { + throw new ConflictException("Пользователь должен быть инициатором"); + } + + return EventMapper.toEventFullDto( + event, + getConfirmedRequests(event.getId()), + getHits(event.getId()) + ); + } + + @Override + public EventFullDto patchEvent(Long userId, Long eventId, @NonNull UpdateEventUserRequest request) { + if (request.participantLimit() != null && request.participantLimit() < 0) { + throw new ValidationException("Ограничение на количество участников должно быть положительным числом"); + } + + checkUser(userId); + return patchEvent(eventId, request, 2, false); + } + + private EventFullDto patchEvent(Long eventId, @NonNull UpdateEventUserRequest request, long hoursBeforeStart, + boolean isAdmin) { + try { + Event event = getEventById(eventId); + + if (!isAdmin && event.getState() == EventState.PUBLISHED) { + throw new ConflictException("Нельзя редактировать опубликованное событие"); + } + + if (request.eventDate() != null) { + LocalDateTime eventDateTime = request.eventDate(); + LocalDateTime minDateTime = LocalDateTime.now().plusHours(hoursBeforeStart); + + if (!isAdmin && eventDateTime.isBefore(minDateTime)) { + throw new ValidationException( + String.format("Дата события должна быть не ранее чем за %d часа(ов) до начала", + hoursBeforeStart) + ); + } + } + + UserStateAction action = request.stateAction(); + + if (action != null) { + EventState newState = isAdmin + ? StateMapper.mapAdminEventAction(action) + : StateMapper.mapUserEventAction(action); + + if (EventState.PUBLISHED.equals(newState)) { + event.setPublishedOn(LocalDateTime.from(Instant.now())); + } + if (newState != null) { + event.setState(newState); + } + } + + if (request.category() != null) { + Category category = getCategoryById(request.category()); + event.setCategory(category); + } + + EventMapper.merge(event, request); + + Event patched = eventRepository.save(event); + + log.info("Ивент обновлен: {}", patched.getId()); + + return EventMapper.toEventFullDto( + event, + getConfirmedRequests(event.getId()), + getHits(event.getId()) + ); + + } catch (DataIntegrityViolationException e) { + log.debug("Конфликт вовремя обновления ивента {}", request, e); + throw new ConflictException("Конфликт с другим ивентом"); + } + } + + private void checkUser(Long userId) { + if (!userRepository.existsById(userId)) { + throw new NotFoundException("Пользователь с id=" + userId + " не найден"); + } + } + + @NonNull + private User getUserById(long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException("Пользователь с id=" + userId + " не найден")); + } + + @NonNull + private Event getEventById(long eventId) { + return eventRepository.findById(eventId).orElseThrow( + () -> new NotFoundException("Событие с id=" + eventId + " не найдено") + ); + } + + @NonNull + private Category getCategoryById(long categoryId) { + return categoryRepository.findById(categoryId).orElseThrow( + () -> new NotFoundException("Категория с id=" + categoryId + " не найдена") + ); + } + + private long getConfirmedRequests(Long eventId) { + return requestRepository.countByEventIdAndStatus(eventId, ParticipationStatus.CONFIRMED); + } + + private long getHits(long eventId) { + List stats = statRepository.getStat(List.of("/events/" + eventId), true); + if (stats.isEmpty()) { + return 0; + } + return stats.getFirst().getHits(); + } + + private long getHits(@NonNull List stats, long eventId) { + if (stats.isEmpty()) { + return 0; + } + for (ViewStatsDto stat : stats) { + if (stat.getUri().equals("/events/" + eventId)) { + return stat.getHits(); + } + } + return 0; + } + + private long getConfirmedRequests(@NonNull Map requestCountMap, long eventId) { + if (requestCountMap.isEmpty()) { + return 0; + } + if (requestCountMap.containsKey(eventId)) { + return requestCountMap.get(eventId); + } + return 0; + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/service/rating/RatingService.java b/core/main-service/src/main/java/ru/practicum/ewm/service/rating/RatingService.java new file mode 100644 index 0000000..27f61d9 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/service/rating/RatingService.java @@ -0,0 +1,11 @@ +package ru.practicum.ewm.service.rating; + +import ru.practicum.ewm.dto.rating.RatingRequest; +import ru.practicum.ewm.dto.rating.RatingResponse; + +public interface RatingService { + + RatingResponse addOrUpdateReaction(Long userId, Long eventId, RatingRequest request); + + void removeReaction(Long userId, Long eventId); +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/service/rating/RatingServiceImpl.java b/core/main-service/src/main/java/ru/practicum/ewm/service/rating/RatingServiceImpl.java new file mode 100644 index 0000000..df506dc --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/service/rating/RatingServiceImpl.java @@ -0,0 +1,94 @@ +package ru.practicum.ewm.service.rating; + +import jakarta.validation.ValidationException; +import lombok.RequiredArgsConstructor; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.ewm.dao.EventRepository; +import ru.practicum.ewm.dao.RatingRepository; +import ru.practicum.ewm.dao.UserRepository; +import ru.practicum.ewm.dto.rating.RatingRequest; +import ru.practicum.ewm.dto.rating.RatingResponse; +import ru.practicum.ewm.model.Event; +import ru.practicum.ewm.model.Rating; +import ru.practicum.ewm.model.User; +import ru.practicum.ewm.model.enums.EventState; +import ru.practicum.ewm.model.enums.Reaction; +import ru.practicum.ewm.util.error.exception.ConflictException; +import ru.practicum.ewm.util.error.exception.NotFoundException; + +import java.util.Objects; + +@Service +@RequiredArgsConstructor +@Transactional +public class RatingServiceImpl implements RatingService { + + private final RatingRepository ratingRepository; + private final UserRepository userRepository; + private final EventRepository eventRepository; + + @Override + public RatingResponse addOrUpdateReaction(Long userId, Long eventId, RatingRequest request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException("Пользователь с id=" + userId + " не найден")); + Event event = eventRepository.findByIdAndState(eventId, EventState.PUBLISHED) + .orElseThrow(() -> + new NotFoundException("Событие с id=" + eventId + " не существует или не опубликовано.") + ); + User initiator = event.getInitiator(); + if (Objects.equals(user.getId(), initiator.getId())) { + throw new ValidationException("Нельзя ставить реакции своим событиям"); + } + + Rating rating = ratingRepository.findByUserIdAndEventId(userId, eventId).orElse(null); + + if (rating != null) { + if (rating.getReaction() == request.getReaction()) { + ratingRepository.delete(rating); + updateEventRate(event); + throw new ConflictException("Reaction removed"); + } else { + rating.setReaction(request.getReaction()); + ratingRepository.save(rating); + updateEventRate(event); + return mapToResponse(rating); + } + } else { + rating = Rating.builder() + .user(user) + .event(event) + .reaction(request.getReaction()) + .build(); + ratingRepository.save(rating); + updateEventRate(event); + return mapToResponse(rating); + } + } + + @Override + public void removeReaction(Long userId, Long eventId) { + Rating rating = ratingRepository.findByUserIdAndEventId(userId, eventId) + .orElseThrow(() -> new NotFoundException("Реакция не найдена")); + ratingRepository.delete(rating); + Event event = rating.getEvent(); + updateEventRate(event); + } + + private void updateEventRate(@NonNull Event event) { + long likes = ratingRepository.countByEventIdAndReaction(event.getId(), Reaction.LIKE); + long dislikes = ratingRepository.countByEventIdAndReaction(event.getId(), Reaction.DISLIKE); + event.setRate(likes - dislikes); + eventRepository.save(event); + } + + private RatingResponse mapToResponse(@NonNull Rating rating) { + return RatingResponse.builder() + .id(rating.getId()) + .userId(rating.getUser().getId()) + .eventId(rating.getEvent().getId()) + .reaction(rating.getReaction()) + .build(); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/service/request/EventRequestCount.java b/core/main-service/src/main/java/ru/practicum/ewm/service/request/EventRequestCount.java new file mode 100644 index 0000000..a7d0684 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/service/request/EventRequestCount.java @@ -0,0 +1,9 @@ +package ru.practicum.ewm.service.request; + + +public interface EventRequestCount { + + Long getEventId(); + + Long getCount(); +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/service/request/RequestService.java b/core/main-service/src/main/java/ru/practicum/ewm/service/request/RequestService.java new file mode 100644 index 0000000..50cff58 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/service/request/RequestService.java @@ -0,0 +1,23 @@ +package ru.practicum.ewm.service.request; + +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.ewm.dto.request.EventRequestStatusUpdateRequest; +import ru.practicum.ewm.dto.request.EventRequestStatusUpdateResult; +import ru.practicum.ewm.dto.request.ParticipationRequestDto; + +import java.util.List; + +@Transactional +public interface RequestService { + + List findByEventId(Long userId, Long eventId); + + EventRequestStatusUpdateResult updateStatusRequest(Long userId, Long eventId, + EventRequestStatusUpdateRequest request); + + List findByRequesterId(Long userId); + + ParticipationRequestDto addParticipationRequest(Long userId, Long eventId); + + ParticipationRequestDto cancelParticipationRequest(Long userId, Long requestId); +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/service/request/RequestServiceImpl.java b/core/main-service/src/main/java/ru/practicum/ewm/service/request/RequestServiceImpl.java new file mode 100644 index 0000000..bed101c --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/service/request/RequestServiceImpl.java @@ -0,0 +1,198 @@ +package ru.practicum.ewm.service.request; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.experimental.FieldDefaults; +import lombok.extern.slf4j.Slf4j; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.ewm.dao.EventRepository; +import ru.practicum.ewm.dao.RequestRepository; +import ru.practicum.ewm.dao.UserRepository; +import ru.practicum.ewm.dto.request.EventRequestStatusUpdateRequest; +import ru.practicum.ewm.dto.request.EventRequestStatusUpdateResult; +import ru.practicum.ewm.dto.request.ParticipationRequestDto; +import ru.practicum.ewm.mapper.RequestMapper; +import ru.practicum.ewm.model.Event; +import ru.practicum.ewm.model.ParticipationRequest; +import ru.practicum.ewm.model.User; +import ru.practicum.ewm.model.enums.EventState; +import ru.practicum.ewm.model.enums.ParticipationStatus; +import ru.practicum.ewm.util.error.exception.ConflictException; +import ru.practicum.ewm.util.error.exception.NotFoundException; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +public class RequestServiceImpl implements RequestService { + + RequestRepository requestRepository; + UserRepository userRepository; + EventRepository eventRepository; + + public List findByEventId(Long userId, Long eventId) { + Event event = getEventById(eventId); + if (!event.getInitiator().getId().equals(userId)) { + throw new NotFoundException("Событие не найдено"); + } + + return requestRepository.findByEventId(eventId) + .stream() + .map(RequestMapper::toParticipationRequestDto) + .toList(); + } + + public EventRequestStatusUpdateResult updateStatusRequest(Long userId, Long eventId, + EventRequestStatusUpdateRequest request) { + Event event = getEventById(eventId); + if (!event.getInitiator().getId().equals(userId)) { + throw new NotFoundException("Событие не найдено"); + } + + int limit = event.getParticipantLimit(); + List confirmedRequests = new ArrayList<>(); + List rejectedRequests = new ArrayList<>(); + + boolean isModerationOff = !event.isRequestModeration() || limit == 0; + boolean idsEmpty = request.requestIds() == null || request.requestIds().isEmpty(); + + if (isModerationOff || idsEmpty) { + return EventRequestStatusUpdateResult.builder() + .confirmedRequests(Collections.emptyList()) + .rejectedRequests(Collections.emptyList()) + .build(); + } + + int countConfirmed = requestRepository.countByEventIdAndStatus(eventId, ParticipationStatus.CONFIRMED); + List requests = requestRepository.findAllByIdIn(request.requestIds()); + + if (request.status().name().equals(ParticipationStatus.CONFIRMED.name()) && countConfirmed >= limit) { + throw new ConflictException("Достигнут лимит подтвержденных заявок"); + } + + for (ParticipationRequest pr : requests) { + if (!pr.getStatus().equals(ParticipationStatus.PENDING)) { + throw new ConflictException("Статус можно изменить только у заявок в состоянии рассмотрения"); + } + + if (request.status().name().equals(ParticipationStatus.CONFIRMED.name()) && countConfirmed < limit) { + pr.setStatus(ParticipationStatus.CONFIRMED); + countConfirmed++; + confirmedRequests.add(RequestMapper.toParticipationRequestDto(pr)); + } else { + pr.setStatus(ParticipationStatus.REJECTED); + rejectedRequests.add(RequestMapper.toParticipationRequestDto(pr)); + } + } + + requestRepository.saveAll(requests); + + // если в процессе лимит превышен - отклоняем все оставшиеся заявки + if (request.status().name().equals(ParticipationStatus.CONFIRMED.name()) && countConfirmed >= limit) { + if (requestRepository.rejectPendingRequests(eventId, ParticipationStatus.PENDING) < 0) { + throw new RuntimeException("Не удалось отклонить заявку"); + } + } + + return EventRequestStatusUpdateResult.builder() + .confirmedRequests(confirmedRequests) + .rejectedRequests(rejectedRequests) + .build(); + } + + public List findByRequesterId(Long userId) { + return requestRepository.findByRequesterId(userId) + .stream() + .map(RequestMapper::toParticipationRequestDto) + .toList(); + } + + @Transactional + public ParticipationRequestDto addParticipationRequest(Long userId, Long eventId) { + User requester = getUserById(userId); + Event event = getEventById(eventId); + + if (!EventState.PUBLISHED.equals(event.getState())) { + throw new ConflictException("Нельзя участвовать в неопубликованном событии"); + } + + if (requestRepository.existsByRequesterIdAndEventId(userId, eventId)) { + throw new ConflictException("Запрос уже существует"); + } + + if (event.getInitiator().getId().equals(userId)) { + throw new ConflictException("Инициатор события не может добавить запрос на участие в своём событии"); + } + + int limit = event.getParticipantLimit(); + if (limit != 0) { + long confirmedCount = requestRepository.countByEventIdAndStatus(eventId, ParticipationStatus.CONFIRMED); + + if (event.isRequestModeration()) { + long pendingCount = requestRepository.countByEventIdAndStatus(eventId, ParticipationStatus.PENDING); + if (confirmedCount + pendingCount >= limit) { + throw new ConflictException("Достигнут лимит запросов на участие"); + } + } else { + if (confirmedCount >= limit) { + throw new ConflictException("Достигнут лимит запросов на участие"); + } + } + } + + ParticipationStatus status; + + if (!event.isRequestModeration() || limit == 0) { + status = ParticipationStatus.CONFIRMED; + } else { + status = ParticipationStatus.PENDING; + } + + ParticipationRequest request = ParticipationRequest.builder() + .requester(requester) + .event(event) + .status(status) + .created(LocalDateTime.now().truncatedTo(ChronoUnit.MILLIS)) + .build(); + + return RequestMapper.toParticipationRequestDto(requestRepository.save(request)); + } + + public ParticipationRequestDto cancelParticipationRequest(Long userId, Long requestId) { + ParticipationRequest request = getRequestById(requestId); + + if (!request.getRequester().getId().equals(userId)) { + throw new ConflictException("Нельзя отменить чужую заявку"); + } + request.setStatus(ParticipationStatus.CANCELED); + return RequestMapper.toParticipationRequestDto(requestRepository.save(request)); + } + + @NonNull + private User getUserById(long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException("Пользователь с id=" + userId + " не найден")); + } + + @NonNull + private Event getEventById(long eventId) { + return eventRepository.findById(eventId).orElseThrow( + () -> new NotFoundException("Событие с id=" + eventId + " не найдено") + ); + } + + @NonNull + private ParticipationRequest getRequestById(Long requestId) { + return requestRepository.findById(requestId).orElseThrow( + () -> new NotFoundException("Заявка с id=" + requestId + " не найдена") + ); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/service/user/UserService.java b/core/main-service/src/main/java/ru/practicum/ewm/service/user/UserService.java new file mode 100644 index 0000000..d10ee41 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/service/user/UserService.java @@ -0,0 +1,18 @@ +package ru.practicum.ewm.service.user; + +import jakarta.transaction.Transactional; +import ru.practicum.ewm.dto.user.NewUserRequest; +import ru.practicum.ewm.dto.user.UserDto; + +import java.util.List; + +public interface UserService { + + @Transactional + UserDto adminAddNewUser(NewUserRequest newUserRequest); + + List getUsers(List ids, int from, int size); + + @Transactional + void deleteUser(Long userId); +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/service/user/UserServiceImpl.java b/core/main-service/src/main/java/ru/practicum/ewm/service/user/UserServiceImpl.java new file mode 100644 index 0000000..31cd7cd --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/service/user/UserServiceImpl.java @@ -0,0 +1,60 @@ +package ru.practicum.ewm.service.user; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.ewm.dao.UserRepository; +import ru.practicum.ewm.dto.user.NewUserRequest; +import ru.practicum.ewm.dto.user.UserDto; +import ru.practicum.ewm.mapper.UserMapper; +import ru.practicum.ewm.model.User; +import ru.practicum.ewm.util.error.exception.ConflictException; +import ru.practicum.ewm.util.error.exception.NotFoundException; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class UserServiceImpl implements UserService { + + private final UserRepository userRepository; + + @Override + public UserDto adminAddNewUser(@NonNull NewUserRequest newUserRequest) { + if (userRepository.existsByEmail(newUserRequest.email())) { + throw new ConflictException("Пользователь с такой почтой " + newUserRequest.email() + " уже существует"); + } + User user = UserMapper.toEntity(newUserRequest); + return UserMapper.toUserDto(userRepository.save(user)); + } + + @Override + public List getUsers(List ids, int from, int size) { + PageRequest page = PageRequest.of(from / size, size); + List users; + if (ids == null || ids.isEmpty()) { + users = userRepository.findAll(page).getContent(); + } else { + users = userRepository.findAllByIdIn(ids, page); + } + return users.stream() + .map(UserMapper::toUserDto) + .collect(Collectors.toList()); + } + + @Override + @Transactional + public void deleteUser(Long userId) { + checkUser(userId); + userRepository.deleteById(userId); + } + + private void checkUser(Long userId) { + if (!userRepository.existsById(userId)) { + throw new NotFoundException("Пользователь с id=" + userId + " не найден"); + } + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/util/config/MainServiceConfig.java b/core/main-service/src/main/java/ru/practicum/ewm/util/config/MainServiceConfig.java new file mode 100644 index 0000000..a39ef11 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/util/config/MainServiceConfig.java @@ -0,0 +1,27 @@ +package ru.practicum.ewm.util.config; + +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; + +import java.time.format.DateTimeFormatter; + +@SpringBootConfiguration +public class MainServiceConfig { + + @Bean + public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { + String pattern = "yyyy-MM-dd HH:mm:ss"; + return builder -> { + builder.simpleDateFormat(pattern); + builder.serializers(new LocalDateTimeSerializer( + DateTimeFormatter.ofPattern(pattern) + )); + builder.deserializers(new LocalDateTimeDeserializer( + DateTimeFormatter.ofPattern(pattern) + )); + }; + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/util/entity/BaseEntity.java b/core/main-service/src/main/java/ru/practicum/ewm/util/entity/BaseEntity.java new file mode 100644 index 0000000..15f4e23 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/util/entity/BaseEntity.java @@ -0,0 +1,43 @@ +package ru.practicum.ewm.util.entity; + +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@SuperBuilder(toBuilder = true) +@MappedSuperclass +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public abstract class BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + protected Long id; + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + + if (org.hibernate.Hibernate.getClass(this) + != org.hibernate.Hibernate.getClass(o)) { + return false; + } + + BaseEntity other = (BaseEntity) o; + + return id != null && id.equals(other.id); + } + + @Override + public final int hashCode() { + return org.hibernate.Hibernate.getClass(this).hashCode(); + } +} \ No newline at end of file diff --git a/core/main-service/src/main/java/ru/practicum/ewm/util/error/ApiError.java b/core/main-service/src/main/java/ru/practicum/ewm/util/error/ApiError.java new file mode 100644 index 0000000..3d8f3f9 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/util/error/ApiError.java @@ -0,0 +1,22 @@ +package ru.practicum.ewm.util.error; + +import org.springframework.http.HttpStatus; + +import java.time.LocalDateTime; + +/** + * @param status Код статуса HTTP-ответа + * @param reason Общее описание причины ошибки + * @param message Сообщение об ошибке + * @param errors Список стектрейсов или описания ошибок + * @param timestamp Дата и время когда произошла ошибка (в формате "yyyy-MM-dd HH:mm:ss") + */ +public record ApiError( + HttpStatus status, + String reason, + String message, + String errors, + LocalDateTime timestamp +) { +} + diff --git a/core/main-service/src/main/java/ru/practicum/ewm/util/error/MainServiceErrorHandler.java b/core/main-service/src/main/java/ru/practicum/ewm/util/error/MainServiceErrorHandler.java new file mode 100644 index 0000000..a31a2c5 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/util/error/MainServiceErrorHandler.java @@ -0,0 +1,182 @@ +package ru.practicum.ewm.util.error; + +import jakarta.validation.ValidationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.lang.NonNull; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.HandlerMethodValidationException; +import ru.practicum.ewm.util.error.exception.ConflictException; +import ru.practicum.ewm.util.error.exception.HitRequestException; +import ru.practicum.ewm.util.error.exception.NotFoundException; +import ru.practicum.ewm.util.error.exception.StatResponseException; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.time.LocalDateTime; +import java.util.Objects; + +import static org.springframework.http.HttpStatus.*; + +@Slf4j +@RestControllerAdvice +public class MainServiceErrorHandler { + + @ExceptionHandler + @ResponseStatus(INTERNAL_SERVER_ERROR) + public ApiError handleSomeError(final Exception ex) { + String stackTrace = getStackTrace(ex); + return new ApiError( + INTERNAL_SERVER_ERROR, + "Непредвиденная ошибка", + ex.getMessage(), + stackTrace, + LocalDateTime.now() + ); + } + + @ExceptionHandler + @ResponseStatus(INTERNAL_SERVER_ERROR) + public ApiError handleHitRequestException(HitRequestException e) { + log.info("500 {}", e.getMessage(), e); + String stackTrace = getStackTrace(e); + return new ApiError( + INTERNAL_SERVER_ERROR, + "Stat-server error ....", + e.getMessage(), + stackTrace, + LocalDateTime.now() + ); + } + + @ExceptionHandler + @ResponseStatus(INTERNAL_SERVER_ERROR) + public ApiError handleStatResponseException(StatResponseException ex) { + log.info("500 {}", ex.getMessage(), ex); + String stackTrace = getStackTrace(ex); + return new ApiError( + INTERNAL_SERVER_ERROR, + "Stat-server error ....", + ex.getMessage(), + stackTrace, + LocalDateTime.now() + ); + } + + @ExceptionHandler + @ResponseStatus(BAD_REQUEST) + public ApiError handleMissingServletRequestParameter(@NonNull final MissingServletRequestParameterException ex) { + log.warn("400 Bad Request (MissingServletRequestParameter): {}", ex.getMessage()); + String stackTrace = getStackTrace(ex); + return new ApiError( + BAD_REQUEST, + "MissingServletRequestParameter", + ex.getMessage(), + stackTrace, + LocalDateTime.now() + ); + } + + @ExceptionHandler + @ResponseStatus(BAD_REQUEST) + public ApiError handleMethodArgumentNotValid(@NonNull MethodArgumentNotValidException ex) { + log.warn("400 Bad Request (ArgumentNotValid): {}", ex.getMessage()); + return new ApiError( + BAD_REQUEST, + "Incorrectly made request.", + "Field: " + Objects.requireNonNull(ex.getBindingResult().getFieldError()).getField() + + ". Error: " + ex.getBindingResult().getFieldError().getDefaultMessage(), + getStackTrace(ex), + LocalDateTime.now() + ); + } + + @ExceptionHandler + @ResponseStatus(BAD_REQUEST) + public ApiError handleHandlerMethodValidationException(@NonNull HandlerMethodValidationException ex) { + log.warn("400 Bad Request (HandlerMethodValidationException): {}", ex.getMessage()); + return new ApiError( + BAD_REQUEST, + "HandlerMethodValidationException", + ex.getMessage(), + getStackTrace(ex), + LocalDateTime.now() + ); + } + + @ExceptionHandler + @ResponseStatus(BAD_REQUEST) + public ApiError handleIllegalArgumentException(@NonNull IllegalArgumentException ex) { + log.warn("400 Bad Request (IllegalArgument): {}", ex.getMessage()); + return new ApiError( + BAD_REQUEST, + "Incorrectly made request.", + ex.getMessage(), + getStackTrace(ex), + LocalDateTime.now() + ); + } + + @ExceptionHandler + @ResponseStatus(BAD_REQUEST) + public ApiError handleValidationException(@NonNull ValidationException ex) { + log.warn("400 Bad Request (ValidationException): {}", ex.getMessage()); + return new ApiError( + BAD_REQUEST, + "Incorrectly made request.", + ex.getMessage(), + getStackTrace(ex), + LocalDateTime.now() + ); + } + + @ExceptionHandler + @ResponseStatus(NOT_FOUND) + public ApiError handleNotFound(@NonNull NotFoundException ex) { + log.warn("404 Not Found: {}", ex.getMessage()); + return new ApiError( + NOT_FOUND, + "The required object was not found.", + ex.getMessage(), + getStackTrace(ex), + LocalDateTime.now() + ); + } + + @ExceptionHandler + @ResponseStatus(CONFLICT) + public ApiError handleConflict(@NonNull ConflictException ex) { + log.warn("409 Conflict: {}", ex.getMessage()); + return new ApiError( + CONFLICT, + "For the requested operation the conditions are not met.", + ex.getMessage(), + getStackTrace(ex), + LocalDateTime.now() + ); + } + + @ExceptionHandler + @ResponseStatus(CONFLICT) + public ApiError handleHttpMessageNotReadableException(@NonNull final HttpMessageNotReadableException ex) { + log.warn("409 CONFLICT: Required request body is missing or invalid"); + return new ApiError( + BAD_REQUEST, + "Incorrectly made request.", + ex.getMessage(), + getStackTrace(ex), + LocalDateTime.now() + ); + } + + private String getStackTrace(@NonNull Exception e) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + return sw.toString(); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/util/error/exception/ConflictException.java b/core/main-service/src/main/java/ru/practicum/ewm/util/error/exception/ConflictException.java new file mode 100644 index 0000000..8848991 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/util/error/exception/ConflictException.java @@ -0,0 +1,8 @@ +package ru.practicum.ewm.util.error.exception; + +public class ConflictException extends RuntimeException { + + public ConflictException(String message) { + super(message); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/util/error/exception/HitRequestException.java b/core/main-service/src/main/java/ru/practicum/ewm/util/error/exception/HitRequestException.java new file mode 100644 index 0000000..a0232af --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/util/error/exception/HitRequestException.java @@ -0,0 +1,8 @@ +package ru.practicum.ewm.util.error.exception; + +public class HitRequestException extends RuntimeException { + + public HitRequestException(Exception ex) { + super("Отправка статистики не удалась " + ex.getMessage(), ex); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/util/error/exception/NotFoundException.java b/core/main-service/src/main/java/ru/practicum/ewm/util/error/exception/NotFoundException.java new file mode 100644 index 0000000..a9d3922 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/util/error/exception/NotFoundException.java @@ -0,0 +1,8 @@ +package ru.practicum.ewm.util.error.exception; + +public class NotFoundException extends RuntimeException { + + public NotFoundException(String message) { + super(message); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/util/error/exception/StatResponseException.java b/core/main-service/src/main/java/ru/practicum/ewm/util/error/exception/StatResponseException.java new file mode 100644 index 0000000..f2b13cb --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/util/error/exception/StatResponseException.java @@ -0,0 +1,14 @@ +package ru.practicum.ewm.util.error.exception; + +public class StatResponseException extends RuntimeException { + + private static final String MESSAGE = "Не удалось получить статистику"; + + public StatResponseException(Exception ex) { + super(MESSAGE, ex); + } + + public StatResponseException() { + super(MESSAGE); + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/util/specification/EventSpecifications.java b/core/main-service/src/main/java/ru/practicum/ewm/util/specification/EventSpecifications.java new file mode 100644 index 0000000..fe01617 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/util/specification/EventSpecifications.java @@ -0,0 +1,130 @@ +package ru.practicum.ewm.util.specification; + +import jakarta.persistence.criteria.Join; +import lombok.experimental.UtilityClass; +import org.springframework.data.jpa.domain.Specification; +import ru.practicum.ewm.model.Category; +import ru.practicum.ewm.model.Event; +import ru.practicum.ewm.model.User; +import ru.practicum.ewm.model.enums.EventState; + +import java.time.LocalDateTime; +import java.util.List; + +@UtilityClass +public class EventSpecifications { + + /// Поиск по тексту + public Specification textContains(String text) { + return (root, query, cb) -> { + if (text == null || text.isBlank()) { + return null; + } + + String pattern = "%" + text.toLowerCase() + "%"; + + return cb.or( + cb.like(cb.lower(root.get("annotation")), pattern), + cb.like(cb.lower(root.get("description")), pattern) + ); + }; + } + + /// Категории + public Specification hasCategories(List categories) { + return (root, query, cb) -> { + if (categories == null || categories.isEmpty()) { + return null; + } + + Join categoryJoin = root.join("category"); + + return categoryJoin.get("id").in(categories); + }; + } + + /// Платное / бесплатное + public Specification isPaid(Boolean paid) { + return (root, query, cb) -> { + if (paid == null) { + return null; + } + + return cb.equal(root.get("paid"), paid); + }; + } + + /// Дата начала + public Specification dateAfter(LocalDateTime rangeStart) { + return (root, query, cb) -> { + if (rangeStart == null) { + return null; + } + + return cb.greaterThanOrEqualTo(root.get("eventDate"), rangeStart); + }; + } + + /// Дата окончания + public Specification dateBefore(LocalDateTime rangeEnd) { + return (root, query, cb) -> { + if (rangeEnd == null) { + return null; + } + + return cb.lessThanOrEqualTo(root.get("eventDate"), rangeEnd); + }; + } + + /// Только доступные события + public Specification onlyAvailable(Boolean onlyAvailable) { + return (root, query, cb) -> { + if (!Boolean.TRUE.equals(onlyAvailable)) { + return null; + } + + return cb.or( + cb.equal(root.get("participantLimit"), 0), + cb.lessThan( + root.get("confirmedRequests"), + root.get("participantLimit") + ) + ); + }; + } + + /// Статус = PUBLISHED + public Specification isPublished() { + return (root, query, cb) -> + cb.equal(root.get("state"), EventState.PUBLISHED); + } + + /// Дата в будущем + public Specification eventDateAfterNow(LocalDateTime now) { + return (root, query, cb) -> + cb.greaterThan(root.get("eventDate"), now); + } + + /// initiator + public static Specification hasUsers(List userIds) { + return (root, query, cb) -> { + if (userIds == null || userIds.isEmpty()) { + return null; + } + + Join userJoin = root.join("initiator"); + return userJoin.get("id").in(userIds); + }; + } + + /// По статусам + public static Specification hasStates(List states) { + return (root, query, cb) -> { + if (states == null || states.isEmpty()) { + return null; + } + + return root.get("state").in(states); + }; + } +} \ No newline at end of file diff --git a/core/main-service/src/main/java/ru/practicum/ewm/util/specification/SpecBuilder.java b/core/main-service/src/main/java/ru/practicum/ewm/util/specification/SpecBuilder.java new file mode 100644 index 0000000..1ec02c1 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/util/specification/SpecBuilder.java @@ -0,0 +1,36 @@ +package ru.practicum.ewm.util.specification; + +import org.springframework.data.jpa.domain.Specification; + +import java.util.function.Supplier; + +public class SpecBuilder { + + private Specification spec; + + private SpecBuilder() { + this.spec = (root, query, cb) -> cb.conjunction(); + } + + public static SpecBuilder builder() { + return new SpecBuilder<>(); + } + + public SpecBuilder andIf(boolean condition, Supplier> supplier) { + if (condition) { + spec = spec.and(supplier.get()); + } + return this; + } + + public SpecBuilder and(Specification other) { + if (other != null) { + spec = spec.and(other); + } + return this; + } + + public Specification build() { + return spec; + } +} diff --git a/core/main-service/src/main/java/ru/practicum/ewm/util/statistic/StatRepository.java b/core/main-service/src/main/java/ru/practicum/ewm/util/statistic/StatRepository.java new file mode 100644 index 0000000..0f9d137 --- /dev/null +++ b/core/main-service/src/main/java/ru/practicum/ewm/util/statistic/StatRepository.java @@ -0,0 +1,85 @@ +package ru.practicum.ewm.util.statistic; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import ru.practicum.ewm.util.error.exception.HitRequestException; +import ru.practicum.ewm.util.error.exception.StatResponseException; +import ru.practicum.stat.client.StatClient; +import ru.practicum.stat.dto.EndpointHitDto; +import ru.practicum.stat.dto.StatsRequest; +import ru.practicum.stat.dto.ViewStatsDto; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class StatRepository { + + private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private final LocalDateTime startUnixEpoch = LocalDateTime.parse("1970-01-01T00:00:00"); + + @Value("${spring.application.name}") + private String appName; + + private final StatClient statClient; + + public void sendHitRequest(HttpServletRequest request) { + try { + statClient.hit(EndpointHitDto.builder() + .ip(request.getRemoteAddr()) + .uri(request.getRequestURI()) + .app(appName) + .timestamp(LocalDateTime.now()) + .build() + ); + } catch (Exception ex) { + throw new HitRequestException(ex); + } + } + + public List getStat(List statUris, + LocalDateTime rangeStart, + LocalDateTime rangeEnd, + boolean uniqe) { + if (statUris == null || statUris.isEmpty()) { + throw new StatResponseException(); + } + + if (rangeStart == null) { + rangeStart = startUnixEpoch; + } + + if (rangeEnd == null) { + rangeEnd = LocalDateTime.now(); + } + + StatsRequest request = StatsRequest.builder() + .uris(statUris) + .start(rangeStart.truncatedTo(ChronoUnit.MILLIS).format(formatter)) + .end(rangeEnd.truncatedTo(ChronoUnit.MILLIS).format(formatter)) + .unique(uniqe) + .build(); + + List stats; + try { + stats = statClient.getStat(request); + } catch (Exception ex) { + throw new StatResponseException(ex); + } + + if (stats == null) { + throw new StatResponseException(); + } + + return stats; + } + + public List getStat(List statUris, boolean uniqe) { + return getStat(statUris, null, null, uniqe); + } +} diff --git a/core/main-service/src/main/resources/application-test.yaml b/core/main-service/src/main/resources/application-test.yaml new file mode 100644 index 0000000..b8c881d --- /dev/null +++ b/core/main-service/src/main/resources/application-test.yaml @@ -0,0 +1,10 @@ +spring: + jpa: + hibernate: + ddl-auto: none + sql: + init: + schema-locations: classpath:schema-h2.sql + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:ewm-main diff --git a/core/main-service/src/main/resources/application.yml b/core/main-service/src/main/resources/application.yml new file mode 100644 index 0000000..8c9a66f --- /dev/null +++ b/core/main-service/src/main/resources/application.yml @@ -0,0 +1,31 @@ +server: + port: 0 + +spring: + application: + name: main-service + config: + import: "optional:configserver:" + cloud: + config: + discovery: + enabled: true + service-id: config-server + fail-fast: true + retry: + initial-interval: 1000 # начальная задержка (мс) + max-interval: 10000 # максимальная задержка (мс) + multiplier: 2.0 # множитель увеличения задержки + max-attempts: 20 # количество попыток (0 = бесконечно) + +eureka: + client: + serviceUrl: + defaultZone: http://localhost:8761/eureka/ + register-with-eureka: true + fetch-registry: true + instance: + prefer-ip-address: true + instance-id: ${spring.application.name}:${random.value} + lease-renewal-interval-in-seconds: 5 + lease-expiration-duration-in-seconds: 10 diff --git a/core/main-service/src/main/resources/schema-h2.sql b/core/main-service/src/main/resources/schema-h2.sql new file mode 100644 index 0000000..06d81b3 --- /dev/null +++ b/core/main-service/src/main/resources/schema-h2.sql @@ -0,0 +1,93 @@ +-- generated by gpt +CREATE TABLE IF NOT EXISTS users ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255) NOT NULL, + name VARCHAR(255) NOT NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users (email); + +CREATE TABLE IF NOT EXISTS categories ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(50) NOT NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_categories_name ON categories (name); + +CREATE TABLE IF NOT EXISTS events ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + annotation VARCHAR(2000) NOT NULL, + created_on TIMESTAMP NOT NULL, + description VARCHAR(7000) NOT NULL, + event_date TIMESTAMP NOT NULL, + location CLOB, + paid BOOLEAN NOT NULL, + participant_limit INT NOT NULL, + published_on TIMESTAMP, + request_moderation BOOLEAN NOT NULL, + state VARCHAR(255) NOT NULL, + title VARCHAR(120) NOT NULL, + category_id BIGINT, + initiator_id BIGINT, + rate BIGINT NOT NULL DEFAULT 0, + + CONSTRAINT fk_events_users + FOREIGN KEY (initiator_id) + REFERENCES users(id), + + CONSTRAINT fk_events_categories + FOREIGN KEY (category_id) + REFERENCES categories(id), + + CONSTRAINT chk_events_state + CHECK (state IN ('PENDING','PUBLISHED','CANCELED')) +); + +CREATE TABLE IF NOT EXISTS requests ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + created TIMESTAMP, + status VARCHAR(255), + + event_id BIGINT, + requester_id BIGINT, + + CONSTRAINT fk_requests_users + FOREIGN KEY (requester_id) + REFERENCES users(id), + + CONSTRAINT fk_requests_events + FOREIGN KEY (event_id) + REFERENCES events(id), + + CONSTRAINT chk_requests_status + CHECK (status IN ('PENDING', 'CONFIRMED', 'REJECTED', 'CANCELED')) +); + +CREATE TABLE IF NOT EXISTS compilations ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + pinned BOOLEAN NOT NULL, + title VARCHAR(50) NOT NULL +); + +CREATE TABLE IF NOT EXISTS compilation_events ( + compilations_id BIGINT NOT NULL, + events_id BIGINT NOT NULL, + + CONSTRAINT fk_events_compilations + FOREIGN KEY (compilations_id) + REFERENCES compilations(id) + ON DELETE CASCADE, + + CONSTRAINT fk_compilations_events + FOREIGN KEY (events_id) + REFERENCES events(id) + ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS ratings ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NOT NULL, + event_id BIGINT NOT NULL, + reaction VARCHAR(10) NOT NULL CHECK (reaction IN ('LIKE', 'DISLIKE')), + CONSTRAINT uq_ratings_user_event UNIQUE (user_id, event_id), + CONSTRAINT fk_ratings_users FOREIGN KEY (user_id) REFERENCES users(id), + CONSTRAINT fk_ratings_events FOREIGN KEY (event_id) REFERENCES events(id) +); \ No newline at end of file diff --git a/core/main-service/src/main/resources/schema-postgres.sql b/core/main-service/src/main/resources/schema-postgres.sql new file mode 100644 index 0000000..eafdc8b --- /dev/null +++ b/core/main-service/src/main/resources/schema-postgres.sql @@ -0,0 +1,93 @@ +CREATE TABLE IF NOT EXISTS public.users ( + id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + email varchar(255) NOT NULL, + name varchar(255) NOT NULL, + CONSTRAINT pk_users PRIMARY KEY (id) +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users (email); + +CREATE TABLE IF NOT EXISTS public.categories ( + id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + name varchar(50) NOT NULL, + CONSTRAINT pk_categories PRIMARY KEY (id) +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_categories_name ON categories (name); + +CREATE TABLE IF NOT EXISTS public.events ( + id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + annotation varchar(2000) NOT NULL, + created_on timestamp(3) without time zone NOT NULL, + description varchar(7000) NOT NULL, + event_date timestamp(3) without time zone NOT NULL, + location jsonb, + paid boolean NOT NULL, + participant_limit integer NOT NULL, + published_on timestamp(3) without time zone, + request_moderation boolean NOT NULL, + state varchar(255) check (state in ('PENDING','PUBLISHED','CANCELED')) NOT NULL, + title varchar(120) NOT NULL, + category_id bigint, + initiator_id bigint, + rate bigint NOT NULL DEFAULT 0, + CONSTRAINT pk_events PRIMARY KEY (id), + CONSTRAINT fk_events_users FOREIGN KEY (initiator_id) + REFERENCES public.users (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION, + CONSTRAINT fk_events_categories FOREIGN KEY (category_id) + REFERENCES public.categories (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION +); + +CREATE TABLE IF NOT EXISTS public.requests ( + id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + created timestamp(3) without time zone, + status varchar(255) check (status in ('PENDING', 'CONFIRMED', 'REJECTED', 'CANCELED')), + event_id bigint, + requester_id bigint, + CONSTRAINT pk_requests PRIMARY KEY (id), + CONSTRAINT fk_requests_users FOREIGN KEY (requester_id) + REFERENCES public.users (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION, + CONSTRAINT fk_requests_events FOREIGN KEY (event_id) + REFERENCES public.events (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION +); + +CREATE TABLE IF NOT EXISTS public.compilations ( + id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + pinned boolean NOT NULL, + title varchar(50) NOT NULL, + CONSTRAINT pk_compilations PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS public.compilation_events ( + compilations_id bigint NOT NULL, + events_id bigint NOT NULL, + CONSTRAINT fk_events_compilations FOREIGN KEY (compilations_id) + REFERENCES public.compilations (id) MATCH SIMPLE + ON DELETE CASCADE, + CONSTRAINT fk_compilations_events FOREIGN KEY (events_id) + REFERENCES public.events (id) MATCH SIMPLE + ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS public.ratings ( + id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + user_id bigint NOT NULL, + event_id bigint NOT NULL, + reaction varchar(10) NOT NULL CHECK (reaction IN ('LIKE', 'DISLIKE')), + CONSTRAINT pk_ratings PRIMARY KEY (id), + CONSTRAINT uq_ratings_user_event UNIQUE (user_id, event_id), + CONSTRAINT fk_ratings_users FOREIGN KEY (user_id) + REFERENCES public.users (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION, + CONSTRAINT fk_ratings_events FOREIGN KEY (event_id) + REFERENCES public.events (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION +); diff --git a/core/main-service/src/test/java/ru/practicum/ewm/TestUtils.java b/core/main-service/src/test/java/ru/practicum/ewm/TestUtils.java new file mode 100644 index 0000000..80e8ba9 --- /dev/null +++ b/core/main-service/src/test/java/ru/practicum/ewm/TestUtils.java @@ -0,0 +1,103 @@ +package ru.practicum.ewm; + +import org.springframework.boot.test.context.TestComponent; +import ru.practicum.ewm.model.Category; +import ru.practicum.ewm.model.Event; +import ru.practicum.ewm.model.Location; +import ru.practicum.ewm.model.User; +import ru.practicum.ewm.model.enums.EventState; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +@SuppressWarnings("unused") +@TestComponent +public class TestUtils { + + public final LocalDateTime pastDate = LocalDateTime.parse("2023-01-01T00:00:01"); + public final LocalDateTime futureDate = LocalDateTime.parse("2028-01-01T00:00:01"); + + private final Random random = new Random(); + + public String generateRandomText(int length, String addAfterStr, Character... addSynbols) { + java.util.List symbols = new ArrayList<>(List.of( + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z' + )); + symbols.addAll(Arrays.asList(addSynbols)); + StringBuilder sb = new StringBuilder(length); + + for (int i = 0; i < length; i++) { + int index = random.nextInt(symbols.size()); + sb.append(symbols.get(index)); + } + + return sb.append(addAfterStr).toString(); + } + + public Location generateLocation() { + return Location.builder() + .lat(random.nextFloat()) + .lon(random.nextFloat()) + .build(); + } + + public User generateUser() { + return User.builder() + .name(generateRandomText(15, "", ' ')) + .email(generateRandomText(15, "@mail.ru")) + .build(); + } + + public Category generateCategory() { + return Category.builder() + .name(generateRandomText(10, "")) + .build(); + } + + public Event generateEvent(User initiator, + Category category, + LocalDateTime eventDate, + EventState eventState) { + return Event.builder() + .annotation(generateRandomText(100, "", ' ')) + .category(category) + .createdOn(eventDate.minusDays(1)) + .description(generateRandomText(200, "", ' ')) + .eventDate(eventDate) + .initiator(initiator) + .location(generateLocation()) + .paid(random.nextBoolean()) + .participantLimit(random.nextInt(100)) + .publishedOn(eventDate.plusHours(3)) + .requestModeration(random.nextBoolean()) + .state(eventState) + .title(generateRandomText(15, "", ' ')) + .rate(0) + .build(); + } + + public Event generateEvent(LocalDateTime eventDate, EventState eventState) { + return generateEvent(generateUser(), generateCategory(), eventDate, eventState); + } + + public Event generateEvent(EventState eventState) { + return generateEvent(pastDate, eventState); + } + + public Event generateEvent() { + return generateEvent(pastDate, EventState.PUBLISHED); + } + + public Event getNewCopyOfEvent(Event event) { + return event.toBuilder().id(null).build(); + } + + public Event getNewCopyOfEvent(Event event, EventState eventState) { + return event.toBuilder().id(null).state(eventState).build(); + } +} \ No newline at end of file diff --git a/core/main-service/src/test/java/ru/practicum/ewm/controller/admin/AdminCategoryControllerTest.java b/core/main-service/src/test/java/ru/practicum/ewm/controller/admin/AdminCategoryControllerTest.java new file mode 100644 index 0000000..58fb804 --- /dev/null +++ b/core/main-service/src/test/java/ru/practicum/ewm/controller/admin/AdminCategoryControllerTest.java @@ -0,0 +1,67 @@ +package ru.practicum.ewm.controller.admin; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.ewm.dto.category.CategoryDto; +import ru.practicum.ewm.dto.category.NewCategoryDto; +import ru.practicum.ewm.service.category.CategoryService; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@ExtendWith(MockitoExtension.class) +class AdminCategoryControllerTest { + + @Mock + private CategoryService categoryService; + + @InjectMocks + private AdminCategoryController controller; + + private MockMvc mockMvc; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + void createCategory_blankName_shouldReturn400() throws Exception { + NewCategoryDto newCat = new NewCategoryDto(""); + + mockMvc.perform(post("/admin/categories") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(newCat))) + .andExpect(status().isBadRequest()); + } + + @Test + void updateCategory_validData_shouldReturn200() throws Exception { + CategoryDto update = CategoryDto.builder().id(1L).name("Выставки").build(); + CategoryDto updated = CategoryDto.builder().id(1L).name("Выставки").build(); + when(categoryService.updateCategory(1L, update)).thenReturn(updated); + + mockMvc.perform(patch("/admin/categories/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(update))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("Выставки")); + } + + @Test + void deleteCategory_existing_shouldReturn204() throws Exception { + mockMvc.perform(delete("/admin/categories/1")) + .andExpect(status().isNoContent()); + } +} \ No newline at end of file diff --git a/core/main-service/src/test/java/ru/practicum/ewm/controller/admin/AdminUserControllerTest.java b/core/main-service/src/test/java/ru/practicum/ewm/controller/admin/AdminUserControllerTest.java new file mode 100644 index 0000000..65dc166 --- /dev/null +++ b/core/main-service/src/test/java/ru/practicum/ewm/controller/admin/AdminUserControllerTest.java @@ -0,0 +1,85 @@ +package ru.practicum.ewm.controller.admin; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.ewm.dto.user.NewUserRequest; +import ru.practicum.ewm.dto.user.UserDto; +import ru.practicum.ewm.service.user.UserService; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@ExtendWith(MockitoExtension.class) +class AdminUserControllerTest { + + @Mock + private UserService userService; + + @InjectMocks + private AdminUserController controller; + + private MockMvc mockMvc; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + void createUser_validRequest_shouldReturn201() throws Exception { + NewUserRequest request = new NewUserRequest("Test User", "user@example.com"); + UserDto userDto = new UserDto(1L, "Test User", "user@example.com"); + when(userService.adminAddNewUser(any(NewUserRequest.class))).thenReturn(userDto); + + mockMvc.perform(post("/admin/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(1L)) + .andExpect(jsonPath("$.name").value("Test User")) + .andExpect(jsonPath("$.email").value("user@example.com")); + } + + @Test + void createUser_blankName_shouldReturn400() throws Exception { + NewUserRequest request = new NewUserRequest("", "user@example.com"); + + mockMvc.perform(post("/admin/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + void getUsers_shouldReturnList() throws Exception { + UserDto user = new UserDto(1L, "Test User", "user@example.com"); + when(userService.getUsers(any(), anyInt(), anyInt())).thenReturn(List.of(user)); + + mockMvc.perform(get("/admin/users") + .param("from", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1L)) + .andExpect(jsonPath("$[0].name").value("Test User")); + } + + @Test + void deleteUser_existingUser_shouldReturn204() throws Exception { + mockMvc.perform(delete("/admin/users/1")) + .andExpect(status().isNoContent()); + } +} \ No newline at end of file diff --git a/core/main-service/src/test/java/ru/practicum/ewm/dto/NewEventDtoTest.java b/core/main-service/src/test/java/ru/practicum/ewm/dto/NewEventDtoTest.java new file mode 100644 index 0000000..bec8633 --- /dev/null +++ b/core/main-service/src/test/java/ru/practicum/ewm/dto/NewEventDtoTest.java @@ -0,0 +1,200 @@ +package ru.practicum.ewm.dto; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import ru.practicum.ewm.dto.event.NewEventDto; +import ru.practicum.ewm.model.Location; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +/// generated by Alisa +class NewEventDtoTest { + + private Validator validator; + + private final String validAnnotation = "Valid annotation text with more than 20 characters"; + private final String validDescription = "Valid description with more than 20 characters and less than 7000 chars"; + + @BeforeEach + void setUp() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + validator = factory.getValidator(); + } + } + + @Test + void whenValidDto_thenNoViolations() { + NewEventDto dto = NewEventDto.builder() + .annotation(validAnnotation) + .category(1L) + .description(validDescription) + .eventDate(LocalDateTime.now().plusDays(1)) + .location(Location.builder() + .lat(55.7558F) + .lon(37.6173F) + .build()) + .paid(true) + .participantLimit(100) + .requestModeration(true) + .title("Valid title") + .build(); + + var violations = validator.validate(dto); + + assertEquals(0, violations.size()); + } + + @Test + void whenAnnotationTooShort_thenViolation() { + NewEventDto dto = NewEventDto.builder() + .annotation("short") + .category(1L) + .description(validDescription) + .eventDate(LocalDateTime.now().plusDays(1)) + .location(Location.builder() + .lat(55.7558F) + .lon(37.6173F) + .build()) + .paid(true) + .participantLimit(100) + .requestModeration(true) + .title("Valid title") + .build(); + + var violations = validator.validate(dto); + + assertEquals(1, violations.size()); + ConstraintViolation violation = violations.iterator().next(); + assertEquals("annotation", violation.getPropertyPath().toString()); + assertEquals("размер должен находиться в диапазоне от 20 до 2000", violation.getMessage()); + } + + @Test + void whenCategoryIsNull_thenViolation() { + NewEventDto dto = NewEventDto.builder() + .annotation(validAnnotation) + .category(null) + .description(validDescription) + .eventDate(LocalDateTime.now().plusDays(1)) + .location(Location.builder() + .lat(55.7558F) + .lon(37.6173F) + .build()) + .paid(true) + .participantLimit(100) + .requestModeration(true) + .title("Valid title") + .build(); + + var violations = validator.validate(dto); + + assertEquals(1, violations.size()); + ConstraintViolation violation = violations.iterator().next(); + assertEquals("category", violation.getPropertyPath().toString()); + assertEquals("не должно равняться null", violation.getMessage()); + } + + @Test + void whenDescriptionTooLong_thenViolation() { + String longDescription = "A".repeat(7001); + NewEventDto dto = NewEventDto.builder() + .annotation(validAnnotation) + .category(1L) + .description(longDescription) + .eventDate(LocalDateTime.now().plusDays(1)) + .location(Location.builder() + .lat(55.7558F) + .lon(37.6173F) + .build()) + .paid(true) + .participantLimit(100) + .requestModeration(true) + .title("Valid title") + .build(); + + var violations = validator.validate(dto); + + assertEquals(1, violations.size()); + ConstraintViolation violation = violations.iterator().next(); + assertEquals("description", violation.getPropertyPath().toString()); + assertEquals("размер должен находиться в диапазоне от 20 до 7000", violation.getMessage()); + } + + @Test + void whenEventDateIsNull_thenViolation() { + NewEventDto dto = NewEventDto.builder() + .annotation(validAnnotation) + .category(1L) + .description(validDescription) + .eventDate(null) + .location(Location.builder() + .lat(55.7558F) + .lon(37.6173F) + .build()) + .paid(true) + .participantLimit(100) + .requestModeration(true) + .title("Valid title") + .build(); + + var violations = validator.validate(dto); + + assertEquals(1, violations.size()); + ConstraintViolation violation = violations.iterator().next(); + assertEquals("eventDate", violation.getPropertyPath().toString()); + assertEquals("не должно равняться null", violation.getMessage()); + } + + @Test + void whenLocationIsNull_thenViolation() { + NewEventDto dto = NewEventDto.builder() + .annotation(validAnnotation) + .category(1L) + .description(validDescription) + .eventDate(LocalDateTime.now().plusDays(1)) + .location(null) + .paid(true) + .participantLimit(100) + .requestModeration(true) + .title("Valid title") + .build(); + + var violations = validator.validate(dto); + + assertEquals(1, violations.size()); + ConstraintViolation violation = violations.iterator().next(); + assertEquals("location", violation.getPropertyPath().toString()); + assertEquals("не должно равняться null", violation.getMessage()); + } + + @Test + void whenTitleTooShort_thenViolation() { + NewEventDto dto = NewEventDto.builder() + .annotation(validAnnotation) + .category(1L) + .description(validDescription) + .eventDate(LocalDateTime.now().plusDays(1)) + .location(Location.builder() + .lat(55.7558F) + .lon(37.6173F) + .build()) + .paid(true) + .participantLimit(100) + .requestModeration(true) + .title("ab") + .build(); + + var violations = validator.validate(dto); + + assertEquals(1, violations.size()); + ConstraintViolation violation = violations.iterator().next(); + assertEquals("title", violation.getPropertyPath().toString()); + assertEquals("размер должен находиться в диапазоне от 3 до 120", violation.getMessage()); + } +} diff --git a/core/main-service/src/test/java/ru/practicum/ewm/service/AdminCategoryServiceTest.java b/core/main-service/src/test/java/ru/practicum/ewm/service/AdminCategoryServiceTest.java new file mode 100644 index 0000000..3da291e --- /dev/null +++ b/core/main-service/src/test/java/ru/practicum/ewm/service/AdminCategoryServiceTest.java @@ -0,0 +1,112 @@ +package ru.practicum.ewm.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import ru.practicum.ewm.dao.CategoryRepository; +import ru.practicum.ewm.dao.EventRepository; +import ru.practicum.ewm.dto.category.CategoryDto; +import ru.practicum.ewm.dto.category.NewCategoryDto; +import ru.practicum.ewm.model.Category; +import ru.practicum.ewm.service.category.CategoryServiceImpl; +import ru.practicum.ewm.util.error.exception.ConflictException; +import ru.practicum.ewm.util.error.exception.NotFoundException; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AdminCategoryServiceTest { + + @Mock + private CategoryRepository categoryRepository; + @Mock + private EventRepository eventRepository; + + @InjectMocks + private CategoryServiceImpl categoryService; + + private Category category; + private NewCategoryDto newCategoryDto; + private CategoryDto updateDto; + + @BeforeEach + void setUp() { + category = Category.builder() + .id(1L) + .name("Концерты") + .build(); + newCategoryDto = new NewCategoryDto("Концерты"); + updateDto = CategoryDto.builder() + .id(1L) + .name("Выставки") + .build(); + } + + @Test + void createCategory_success() { + when(categoryRepository.existsByName("Концерты")).thenReturn(false); + when(categoryRepository.save(any(Category.class))).thenReturn(category); + + CategoryDto result = categoryService.adminAddNewCategory(newCategoryDto); + + assertThat(result.id()).isEqualTo(1L); + assertThat(result.name()).isEqualTo("Концерты"); + } + + @Test + void createCategory_duplicateName_shouldThrowConflict() { + when(categoryRepository.existsByName("Концерты")).thenReturn(true); + + assertThatThrownBy(() -> categoryService.adminAddNewCategory(newCategoryDto)) + .isInstanceOf(ConflictException.class) + .hasMessageContaining("уже существует"); + } + + @Test + void updateCategory_success() { + when(categoryRepository.findById(1L)).thenReturn(Optional.of(category)); + when(categoryRepository.existsByName("Выставки")).thenReturn(false); + when(categoryRepository.save(any(Category.class))).thenReturn(category); + + CategoryDto result = categoryService.updateCategory(1L, updateDto); + + assertThat(result.name()).isEqualTo("Выставки"); + verify(categoryRepository).save(category); + } + + @Test + void updateCategory_notFound_shouldThrowNotFound() { + when(categoryRepository.findById(999L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> categoryService.updateCategory(999L, updateDto)) + .isInstanceOf(NotFoundException.class); + } + + @Test + void deleteCategory_noEvents_success() { + when(categoryRepository.findById(1L)).thenReturn(Optional.of(category)); + when(eventRepository.existsByCategoryId(1L)).thenReturn(false); + + categoryService.deleteCategory(1L); + + verify(categoryRepository).delete(category); + } + + @Test + void deleteCategory_withEvents_shouldThrowConflict() { + when(categoryRepository.findById(1L)).thenReturn(Optional.of(category)); + when(eventRepository.existsByCategoryId(1L)).thenReturn(true); + + assertThatThrownBy(() -> categoryService.deleteCategory(1L)) + .isInstanceOf(ConflictException.class) + .hasMessageContaining("The category is not empty"); + verify(categoryRepository, never()).delete(any()); + } +} \ No newline at end of file diff --git a/core/main-service/src/test/java/ru/practicum/ewm/service/AdminUserServiceTest.java b/core/main-service/src/test/java/ru/practicum/ewm/service/AdminUserServiceTest.java new file mode 100644 index 0000000..ec176d8 --- /dev/null +++ b/core/main-service/src/test/java/ru/practicum/ewm/service/AdminUserServiceTest.java @@ -0,0 +1,101 @@ +package ru.practicum.ewm.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageRequest; +import ru.practicum.ewm.dao.UserRepository; +import ru.practicum.ewm.dto.user.NewUserRequest; +import ru.practicum.ewm.dto.user.UserDto; +import ru.practicum.ewm.model.User; +import ru.practicum.ewm.service.user.UserServiceImpl; +import ru.practicum.ewm.util.error.exception.NotFoundException; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.eq; + +@ExtendWith(MockitoExtension.class) +class AdminUserServiceTest { + + @Mock + private UserRepository userRepository; + + @InjectMocks + private UserServiceImpl userService; + + private User user; + private NewUserRequest newUserRequest; + + @BeforeEach + void setUp() { + user = User.builder() + .id(1L) + .name("Test User") + .email("user@example.com") + .build(); + newUserRequest = NewUserRequest.builder() + .name("Test User") + .email("user@example.com") + .build(); + } + + @Test + void createUser_shouldReturnUserDto() { + when(userRepository.save(any(User.class))).thenReturn(user); + + UserDto result = userService.adminAddNewUser(newUserRequest); + + assertThat(result.id()).isEqualTo(1L); + assertThat(result.name()).isEqualTo("Test User"); + assertThat(result.email()).isEqualTo("user@example.com"); + verify(userRepository).save(any(User.class)); + } + + @Test + void getUsers_withoutIds_shouldReturnAll() { + when(userRepository.findAll(any(PageRequest.class))) + .thenReturn(org.springframework.data.domain.Page.empty()); + + List result = userService.getUsers(null, 0, 10); + + assertThat(result).isEmpty(); + verify(userRepository).findAll(any(PageRequest.class)); + } + + @Test + void getUsers_withIds_shouldFilterByIds() { + when(userRepository.findAllByIdIn(eq(List.of(1L)), any(PageRequest.class))) + .thenReturn(List.of(user)); + + List result = userService.getUsers(List.of(1L), 0, 10); + + assertThat(result).hasSize(1); + assertThat(result.getFirst().id()).isEqualTo(1L); + } + + @Test + void deleteUser_existingUser_shouldSucceed() { + when(userRepository.existsById(1L)).thenReturn(true); + doNothing().when(userRepository).deleteById(1L); + + assertThatCode(() -> userService.deleteUser(1L)).doesNotThrowAnyException(); + verify(userRepository).deleteById(1L); + } + + @Test + void deleteUser_notFound_shouldThrowNotFoundException() { + when(userRepository.existsById(999L)).thenReturn(false); + + assertThatThrownBy(() -> userService.deleteUser(999L)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("Пользователь с id=999 не найден"); + verify(userRepository, never()).deleteById(any()); + } +} \ No newline at end of file diff --git a/core/main-service/src/test/java/ru/practicum/ewm/service/EventServiceImplTest.java b/core/main-service/src/test/java/ru/practicum/ewm/service/EventServiceImplTest.java new file mode 100644 index 0000000..9c97663 --- /dev/null +++ b/core/main-service/src/test/java/ru/practicum/ewm/service/EventServiceImplTest.java @@ -0,0 +1,162 @@ +package ru.practicum.ewm.service; + +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import ru.practicum.ewm.dao.EventRepository; +import ru.practicum.ewm.dao.RequestRepository; +import ru.practicum.ewm.dto.event.EventFullDto; +import ru.practicum.ewm.dto.event.EventShortDto; +import ru.practicum.ewm.dto.event.FreeGetDto; +import ru.practicum.ewm.mapper.EventMapper; +import ru.practicum.ewm.model.Category; +import ru.practicum.ewm.model.Event; +import ru.practicum.ewm.model.Location; +import ru.practicum.ewm.model.User; +import ru.practicum.ewm.model.enums.EventState; +import ru.practicum.ewm.model.enums.ParticipationStatus; +import ru.practicum.ewm.service.event.EventServiceImpl; +import ru.practicum.ewm.util.statistic.StatRepository; +import ru.practicum.stat.dto.ViewStatsDto; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.contains; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class EventServiceImplTest { + + @Mock + StatRepository statRepository; + + @Mock + EventRepository eventRepository; + + @Mock + RequestRepository requestRepository; + + @Mock + HttpServletRequest httpServletRequest; + + @InjectMocks + EventServiceImpl eventService; + + private static Event event; + + @BeforeAll + static void setup() { + event = Event.builder() + .id(1L) + .annotation("annotation".repeat(10)) + .category(Category.builder().id(1L).name("category").build()) + .createdOn(LocalDateTime.parse("2026-11-10T19:00:00")) + .description("description".repeat(10)) + .eventDate(LocalDateTime.parse("2027-01-01T00:00:01")) + .initiator(User.builder().id(1L).name("User").email("user@email.ry").build()) + .location(Location.builder().lat(25.55F).lon(35.77F).build()) + .paid(true) + .participantLimit(30) + .publishedOn(LocalDateTime.parse("2026-12-10T21:00:00")) + .requestModeration(true) + .state(EventState.PUBLISHED) + .title("title") + .rate(0) + .build(); + } + + @Nested + @DisplayName("Получение списка публичных событий") + class GetFreeEvents { + + @Test + void basicFlow() { + // region setup + FreeGetDto getDto = FreeGetDto.builder() + .text("0") + .categories(List.of(0)) + .paid(true) + .onlyAvailable(true) + .rangeStart(LocalDateTime.parse("2026-12-31T23:59:00")) + .rangeEnd(LocalDateTime.parse("2027-01-01T02:00:00")) + .sort(FreeGetDto.FreeEventSort.EVENT_DATE) + .from(0) + .size(1) + .build(); + + Page page = new PageImpl<>(List.of(event)); + // endregion setup + + doNothing().when(statRepository).sendHitRequest(httpServletRequest); + when(eventRepository.findAll( + ArgumentMatchers.>any(), + ArgumentMatchers.any(Pageable.class) + )).thenReturn(page); + + List result = eventService.getFreeEvents(getDto, httpServletRequest); + + assertThat(result, contains(EventMapper.toEventShortDto(event, 0L, 0L))); + verify(statRepository).sendHitRequest(httpServletRequest); + } + + @Test + void whenNoDateRangeProvided() { + FreeGetDto dto = FreeGetDto.builder() + .from(0) + .size(10) + .build(); + + when(eventRepository.findAll( + ArgumentMatchers.>any(), + ArgumentMatchers.any(Pageable.class) + )).thenReturn(Page.empty()); + + eventService.getFreeEvents(dto, httpServletRequest); + + verify(eventRepository).findAll( + ArgumentMatchers.>any(), + ArgumentMatchers.any(Pageable.class) + ); + } + } + + @Nested + @DisplayName("Получение публичного события по id") + class GetFreeEventById { + + @Test + void basicFlow() { + doNothing().when(statRepository).sendHitRequest(httpServletRequest); + when(eventRepository.existsByIdAndState(event.getId(), EventState.PUBLISHED)).thenReturn(Boolean.TRUE); + when(eventRepository.findById(event.getId())).thenReturn(Optional.of(event)); + when(httpServletRequest.getRequestURI()).thenReturn("/events"); + when(statRepository.getStat(List.of("/events"), true)) + .thenReturn(List.of(ViewStatsDto.builder() + .hits(0L) + .build() + )); + when(requestRepository.countByEventIdAndStatus(event.getId(), ParticipationStatus.CONFIRMED)) + .thenReturn(0); + + EventFullDto result = eventService.getFreeEventById(event.getId(), httpServletRequest); + + assertThat(result, is(EventMapper.toEventFullDto(event, 0L, 0L))); + verify(statRepository).sendHitRequest(httpServletRequest); + } + } +} \ No newline at end of file diff --git a/core/pom.xml b/core/pom.xml new file mode 100644 index 0000000..144fd02 --- /dev/null +++ b/core/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + + ru.practicum + explore-with-me + 0.0.1-SNAPSHOT + + + core + pom + + + main-service + + + \ No newline at end of file diff --git a/db-schema.png b/db-schema.png new file mode 100644 index 0000000..677213d Binary files /dev/null and b/db-schema.png differ diff --git a/docker-compose.yml b/docker-compose.yml index be96142..7245a40 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,30 @@ services: - stats-server: + main-db: + image: postgres:16.1 + container_name: ewm-main-db ports: - - "9090:9090" + - "6541:5432" + environment: + - POSTGRES_DB=ewm-main + - POSTGRES_USER=dbuser + - POSTGRES_PASSWORD=12345 + healthcheck: + test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER + timeout: 5s + interval: 5s + retries: 10 stats-db: image: postgres:16.1 - - ewm-service: + container_name: ewm-stats-db ports: - - "8080:8080" - - ewm-db: - image: postgres:16.1 + - "6542:5432" + environment: + - POSTGRES_DB=ewm-stats + - POSTGRES_USER=dbuser + - POSTGRES_PASSWORD=12345 + healthcheck: + test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER + timeout: 5s + interval: 5s + retries: 10 diff --git a/image.png b/image.png new file mode 100644 index 0000000..318bfe5 Binary files /dev/null and b/image.png differ diff --git a/infra/config-repo/application.yml b/infra/config-repo/application.yml new file mode 100644 index 0000000..9b5ac24 --- /dev/null +++ b/infra/config-repo/application.yml @@ -0,0 +1,18 @@ +spring: + main: + banner-mode: off + log-startup-info: false + jackson: + serialization.INDENT_OUTPUT: true + +logging: + level: + root: info + org: + springframework: error + +management: + endpoints: + web: + exposure: + include: health, metrics, prometheus, info, mappings \ No newline at end of file diff --git a/infra/config-repo/core/main-service.yaml b/infra/config-repo/core/main-service.yaml new file mode 100644 index 0000000..15b541d --- /dev/null +++ b/infra/config-repo/core/main-service.yaml @@ -0,0 +1,21 @@ +spring: + sql.init: + mode: ALWAYS + schema-locations: classpath:schema-postgres.sql + datasource: + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://localhost:6541/ewm-main + username: dbuser + password: 12345 + jpa: + generate-ddl: false + hibernate.ddl-auto: validate + show-sql: true + properties: + hibernate: + format_sql: true + +logging: + org: + zalando: + logbook: trace diff --git a/infra/config-repo/infra/gateway-server.yml b/infra/config-repo/infra/gateway-server.yml new file mode 100644 index 0000000..279928a --- /dev/null +++ b/infra/config-repo/infra/gateway-server.yml @@ -0,0 +1,82 @@ +server: + port: 8080 + +spring: + cloud: + gateway: + discovery: + locator: + enabled: false + routes: + + - id: ratings + uri: lb://main-service + predicates: + - Path=/users/*/events/*/likes/** + + - id: user-requests + uri: lb://main-service + predicates: + - Path=/users/*/requests/** + + - id: user-events + uri: lb://main-service + predicates: + - Path=/users/*/events/** + + - id: admin-users + uri: lb://main-service + predicates: + - Path=/admin/users/** + + - id: admin-categories + uri: lb://main-service + predicates: + - Path=/admin/categories/** + + - id: admin-compilations + uri: lb://main-service + predicates: + - Path=/admin/compilations/** + + - id: admin-events + uri: lb://main-service + predicates: + - Path=/admin/events/** + + - id: categories + uri: lb://main-service + predicates: + - Path=/categories/** + + - id: compilations + uri: lb://main-service + predicates: + - Path=/compilations/** + + - id: events + uri: lb://main-service + predicates: + - Path=/events/** + + globalcors: + add-to-simple-url-handler-mapping: true + cors-configurations: + '[/**]': + allowed-origins: + - http://localhost:3000 + - http://localhost:5173 + - http://localhost:8443 + allowed-methods: + - GET + - POST + - PUT + - PATCH + - DELETE + - OPTIONS + allowed-headers: + - '*' + exposed-headers: + - Location + allow-credentials: true + max-age: 3600 diff --git a/infra/config-repo/stats/stats-server.yml b/infra/config-repo/stats/stats-server.yml new file mode 100644 index 0000000..1192cf2 --- /dev/null +++ b/infra/config-repo/stats/stats-server.yml @@ -0,0 +1,16 @@ +spring: + sql: + init: + mode: always + datasource: + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://localhost:6542/ewm-stats + username: dbuser + password: 12345 + jpa: + hibernate: + ddl-auto: none + properties: + hibernate: + format_sql: true + show-sql: true diff --git a/infra/config-server/pom.xml b/infra/config-server/pom.xml new file mode 100644 index 0000000..65ffdf8 --- /dev/null +++ b/infra/config-server/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + + ru.practicum + infra + 0.0.1-SNAPSHOT + + + config-server + jar + + + + org.springframework.cloud + spring-cloud-config-server + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + com.github.ben-manes.caffeine + caffeine + + + org.springframework.retry + spring-retry + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/infra/config-server/src/main/java/ru/practicum/ewm/infra/config/ConfigServer.java b/infra/config-server/src/main/java/ru/practicum/ewm/infra/config/ConfigServer.java new file mode 100644 index 0000000..c41350d --- /dev/null +++ b/infra/config-server/src/main/java/ru/practicum/ewm/infra/config/ConfigServer.java @@ -0,0 +1,13 @@ +package ru.practicum.ewm.infra.config; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.config.server.EnableConfigServer; + +@EnableConfigServer +@SpringBootApplication +public class ConfigServer { + public static void main(String[] args) { + SpringApplication.run(ConfigServer.class, args); + } +} \ No newline at end of file diff --git a/infra/config-server/src/main/resources/application.yml b/infra/config-server/src/main/resources/application.yml new file mode 100644 index 0000000..35e6da8 --- /dev/null +++ b/infra/config-server/src/main/resources/application.yml @@ -0,0 +1,42 @@ +server: + port: 0 + +spring: + main: + banner-mode: off + log-startup-info: false + jackson: + serialization.INDENT_OUTPUT: true + application: + name: config-server + profiles: + active: native + cloud: + config: + discovery: + enabled: true + server: + native: + search-locations: + - file:./infra/config-repo + - file:./infra/config-repo/core + - file:./infra/config-repo/infra + - file:./infra/config-repo/stats + +eureka: + client: + serviceUrl: + defaultZone: http://localhost:8761/eureka/ + register-with-eureka: true + fetch-registry: true + instance: + prefer-ip-address: true + instance-id: ${spring.application.name}:${random.value} + lease-renewal-interval-in-seconds: 5 + lease-expiration-duration-in-seconds: 10 + +logging: + level: + root: info + org: + springframework: error \ No newline at end of file diff --git a/infra/discovery-server/pom.xml b/infra/discovery-server/pom.xml new file mode 100644 index 0000000..98616f4 --- /dev/null +++ b/infra/discovery-server/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + + ru.practicum + infra + 0.0.1-SNAPSHOT + + + discovery-server + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-server + + + org.springframework.retry + spring-retry + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + com.github.ben-manes.caffeine + caffeine + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/infra/discovery-server/src/main/java/ru/practicum/ewm/infra/discovery/DiscoveryServer.java b/infra/discovery-server/src/main/java/ru/practicum/ewm/infra/discovery/DiscoveryServer.java new file mode 100644 index 0000000..6ac142e --- /dev/null +++ b/infra/discovery-server/src/main/java/ru/practicum/ewm/infra/discovery/DiscoveryServer.java @@ -0,0 +1,13 @@ +package ru.practicum.ewm.infra.discovery; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; + +@EnableEurekaServer +@SpringBootApplication +public class DiscoveryServer { + public static void main(String[] args) { + SpringApplication.run(DiscoveryServer.class, args); + } +} \ No newline at end of file diff --git a/infra/discovery-server/src/main/resources/application.yml b/infra/discovery-server/src/main/resources/application.yml new file mode 100644 index 0000000..4d26970 --- /dev/null +++ b/infra/discovery-server/src/main/resources/application.yml @@ -0,0 +1,30 @@ +server: + port: 8761 + +spring: + main: + banner-mode: off + log-startup-info: false + jackson: + serialization.INDENT_OUTPUT: true + application: + name: discovery-server + +eureka: + client: + webclient: + enabled: true + register-with-eureka: false + fetch-registry: false + +logging: + level: + root: info + org: + springframework: error + +management: + endpoints: + web: + exposure: + include: health, metrics, prometheus, info, mappings \ No newline at end of file diff --git a/infra/gateway-server/pom.xml b/infra/gateway-server/pom.xml new file mode 100644 index 0000000..e260520 --- /dev/null +++ b/infra/gateway-server/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + + ru.practicum + infra + 0.0.1-SNAPSHOT + + + gateway-server + jar + + + + org.springframework.cloud + spring-cloud-starter-gateway-server-webflux + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.cloud + spring-cloud-starter-config + + + com.github.ben-manes.caffeine + caffeine + + + org.projectlombok + lombok + provided + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/infra/gateway-server/src/main/java/ru/practicum/ewm/infra/gateway/GatewayServer.java b/infra/gateway-server/src/main/java/ru/practicum/ewm/infra/gateway/GatewayServer.java new file mode 100644 index 0000000..655c9c7 --- /dev/null +++ b/infra/gateway-server/src/main/java/ru/practicum/ewm/infra/gateway/GatewayServer.java @@ -0,0 +1,11 @@ +package ru.practicum.ewm.infra.gateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class GatewayServer { + public static void main(String[] args) { + SpringApplication.run(GatewayServer.class, args); + } +} diff --git a/infra/gateway-server/src/main/resources/application.yml b/infra/gateway-server/src/main/resources/application.yml new file mode 100644 index 0000000..3b2e0a2 --- /dev/null +++ b/infra/gateway-server/src/main/resources/application.yml @@ -0,0 +1,27 @@ +spring: + application: + name: gateway-server + config: + import: "optional:configserver:" + cloud: + config: + discovery: + enabled: true + service-id: config-server + retry: + initial-interval: 1000 + max-interval: 5000 + multiplier: 1.5 + max-attempts: 6 + +eureka: + client: + serviceUrl: + defaultZone: http://localhost:8761/eureka/ + register-with-eureka: true + fetch-registry: true + instance: + prefer-ip-address: true + instance-id: ${spring.application.name}:${random.value} + lease-renewal-interval-in-seconds: 5 + lease-expiration-duration-in-seconds: 10 diff --git a/infra/pom.xml b/infra/pom.xml new file mode 100644 index 0000000..e6130e1 --- /dev/null +++ b/infra/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + + ru.practicum + explore-with-me + 0.0.1-SNAPSHOT + + + infra + pom + + + config-server + discovery-server + gateway-server + + + + + org.springframework.boot + spring-boot-starter-actuator + + + io.micrometer + micrometer-registry-prometheus + + + + \ No newline at end of file diff --git a/lombok.config b/lombok.config index b0056c1..71540ed 100644 --- a/lombok.config +++ b/lombok.config @@ -1,4 +1,10 @@ config.stopBubbling = true lombok.anyconstructor.addconstructorproperties = false lombok.addLombokGeneratedAnnotation = true -lombok.addSuppressWarnings = false \ No newline at end of file +lombok.addSuppressWarnings = false + +# Тустринг содержит родителя с его полями +lombok.toString.callSuper = call + +# Тустринг не использует геттеры +lombok.toString.doNotUseGetters = true diff --git a/pom.xml b/pom.xml index 5b3a851..343ca5b 100644 --- a/pom.xml +++ b/pom.xml @@ -6,22 +6,42 @@ org.springframework.boot spring-boot-starter-parent - 3.3.4 + 3.5.9 Explore With Me + + stats + infra + core + - ru.practicum + ru.practicum explore-with-me 0.0.1-SNAPSHOT pom 21 + 21 + 21 UTF-8 + 2025.0.3 + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + @@ -56,7 +76,7 @@ com.puppycrawl.tools checkstyle - 10.3 + 11.1.0 diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..5a2a350 --- /dev/null +++ b/readme.md @@ -0,0 +1,22 @@ +# Explore with me (микросервисная версия) + +![Static Badge](https://img.shields.io/badge/Java-21-green) +![Static Badge](https://img.shields.io/badge/Spring_Boot-3.5.9-green) +![Static Badge](https://img.shields.io/badge/Lombok-red) +![Static Badge](https://img.shields.io/badge/JPA--specification-8A2BE2) +![Static Badge](https://img.shields.io/badge/RestTemplate-538681) +![Static Badge](https://img.shields.io/badge/PostgreSQL-16.1-blue) +![Static Badge](https://img.shields.io/badge/docker_compose-blue) +![Static Badge](https://img.shields.io/badge/H2_database-blue) +![Static Badge](https://img.shields.io/badge/JUnit-5-orange) +![Static Badge](https://img.shields.io/badge/Mockito-green) +![Static Badge](https://img.shields.io/badge/Jacoco-red) +![Static Badge](https://img.shields.io/badge/Maven-orange) +![Static Badge](https://img.shields.io/badge/Multi--module--project-8A2BE2) + + +## Бэкэнд для сервиса поиска мероприятий +![](image.png) + +### Database map +![db](db-schema.png) \ No newline at end of file diff --git a/stats/pom.xml b/stats/pom.xml new file mode 100644 index 0000000..007e33b --- /dev/null +++ b/stats/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + + ru.practicum + explore-with-me + 0.0.1-SNAPSHOT + + + stats + pom + + + stat-dto + stats-server + stat-client + + + \ No newline at end of file diff --git a/stats/stat-client/pom.xml b/stats/stat-client/pom.xml new file mode 100644 index 0000000..ea100b1 --- /dev/null +++ b/stats/stat-client/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + ru.practicum + stats + 0.0.1-SNAPSHOT + + + stat-client + jar + + + + ru.practicum + stat-dto + 0.0.1-SNAPSHOT + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-web + + + org.apache.httpcomponents.client5 + httpclient5 + + + org.springframework.cloud + spring-cloud-commons + + + org.springframework.retry + spring-retry + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + ${lombok.version} + + + + + + + + \ No newline at end of file diff --git a/stats/stat-client/src/main/java/ru/practicum/stat/client/StatClient.java b/stats/stat-client/src/main/java/ru/practicum/stat/client/StatClient.java new file mode 100644 index 0000000..579f3a5 --- /dev/null +++ b/stats/stat-client/src/main/java/ru/practicum/stat/client/StatClient.java @@ -0,0 +1,127 @@ +package ru.practicum.stat.client; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.lang.NonNull; +import org.springframework.retry.backoff.FixedBackOffPolicy; +import org.springframework.retry.policy.MaxAttemptsRetryPolicy; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; +import ru.practicum.stat.client.exception.StatsServerUnavailable; +import ru.practicum.stat.dto.EndpointHitDto; +import ru.practicum.stat.dto.StatsRequest; +import ru.practicum.stat.dto.ViewStatsDto; + +import java.net.URI; +import java.util.List; + +@Slf4j +@Component +public class StatClient { + + private final RestTemplate rest; + + private final RetryTemplate retryTemplate; + + private final DiscoveryClient discoveryClient; + + public StatClient(@NonNull RestTemplateBuilder builder, + @NonNull DiscoveryClient discoveryClient) { + this.discoveryClient = discoveryClient; + this.retryTemplate = createRetryTemplate(); + this.rest = builder.build(); + } + + public void hit(EndpointHitDto endpointHitDto) { + try { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity requestEntity = new HttpEntity<>(endpointHitDto, headers); + + rest.exchange( + makeUri("/hit"), + HttpMethod.POST, + requestEntity, + Void.class + ); + } catch (Exception e) { + log.error("Ошибка записи: {}", endpointHitDto, e); + } + + } + + public List getStat(StatsRequest statsRequest) { + try { + UriComponentsBuilder builder = UriComponentsBuilder + .fromUri(makeUri("/stats")) + .queryParam("start", statsRequest.getStart()) + .queryParam("end", statsRequest.getEnd()) + .queryParam("unique", statsRequest.getUnique()); + + List uris = statsRequest.getUris(); + + if (uris != null && !uris.isEmpty()) { + builder.queryParam("uris", uris); + } + + URI uri = builder.encode().build().toUri(); + + return rest.exchange( + uri, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() { + } + ).getBody(); + + } catch (Exception e) { + log.error("Ошибка записи: {}", statsRequest, e); + return null; + } + } + + private ServiceInstance getInstance() { + String statsServiceId = "stats-server"; + try { + return discoveryClient + .getInstances(statsServiceId) + .getFirst(); + } catch (Exception exception) { + throw new StatsServerUnavailable( + "Ошибка обнаружения адреса сервиса статистики с id: " + statsServiceId, + exception + ); + } + } + + @NonNull + private URI makeUri(@NonNull String path) { + ServiceInstance instance = retryTemplate.execute(cxt -> getInstance()); + return URI.create("http://" + instance.getHost() + ":" + instance.getPort() + path); + } + + @NonNull + private RetryTemplate createRetryTemplate() { + RetryTemplate retryTemplate = new RetryTemplate(); + + FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy(); + fixedBackOffPolicy.setBackOffPeriod(3000L); + retryTemplate.setBackOffPolicy(fixedBackOffPolicy); + + MaxAttemptsRetryPolicy retryPolicy = new MaxAttemptsRetryPolicy(); + retryPolicy.setMaxAttempts(3); + retryTemplate.setRetryPolicy(retryPolicy); + + return retryTemplate; + } +} diff --git a/stats/stat-client/src/main/java/ru/practicum/stat/client/exception/StatsServerUnavailable.java b/stats/stat-client/src/main/java/ru/practicum/stat/client/exception/StatsServerUnavailable.java new file mode 100644 index 0000000..47d4df3 --- /dev/null +++ b/stats/stat-client/src/main/java/ru/practicum/stat/client/exception/StatsServerUnavailable.java @@ -0,0 +1,7 @@ +package ru.practicum.stat.client.exception; + +public class StatsServerUnavailable extends RuntimeException { + public StatsServerUnavailable(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/stats/stat-dto/pom.xml b/stats/stat-dto/pom.xml new file mode 100644 index 0000000..3a9f1c0 --- /dev/null +++ b/stats/stat-dto/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + + ru.practicum + stats + 0.0.1-SNAPSHOT + + + stat-dto + jar + + + + org.projectlombok + lombok + true + + + com.fasterxml.jackson.core + jackson-annotations + + + jakarta.validation + jakarta.validation-api + 3.0.2 + compile + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + ${lombok.version} + + + + + + + + \ No newline at end of file diff --git a/stats/stat-dto/src/main/java/ru/practicum/stat/dto/EndpointHitDto.java b/stats/stat-dto/src/main/java/ru/practicum/stat/dto/EndpointHitDto.java new file mode 100644 index 0000000..c480d43 --- /dev/null +++ b/stats/stat-dto/src/main/java/ru/practicum/stat/dto/EndpointHitDto.java @@ -0,0 +1,37 @@ +package ru.practicum.stat.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class EndpointHitDto { + @NotBlank(message = "Название приложения не может быть пустым") + @Size(max = 255, message = "APP слишком длинный (макс. 255 символов)") + private String app; + + @NotBlank(message = "URI не может быть пустым") + @Size(max = 512, message = "URI слишком длинный (макс. 512 символов)") + private String uri; + + @NotBlank(message = "IP-адрес не может быть пустым") + @Size(max = 45, message = "IP слишком длинный (макс. 45 символов)") + private String ip; + + @NotNull(message = "TIMESTAMP является обязательным") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime timestamp; + + @Builder.Default + private String actionType = "VIEW"; // VIEW, LIKE, DISLIKE для рейтингов +} diff --git a/stats/stat-dto/src/main/java/ru/practicum/stat/dto/StatsRequest.java b/stats/stat-dto/src/main/java/ru/practicum/stat/dto/StatsRequest.java new file mode 100644 index 0000000..f2fca59 --- /dev/null +++ b/stats/stat-dto/src/main/java/ru/practicum/stat/dto/StatsRequest.java @@ -0,0 +1,27 @@ +package ru.practicum.stat.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class StatsRequest { + + @NotNull + private String start; + + @NotNull + private String end; + + private List uris; + + @Builder.Default + private Boolean unique = false; +} \ No newline at end of file diff --git a/stats/stat-dto/src/main/java/ru/practicum/stat/dto/ViewStatsDto.java b/stats/stat-dto/src/main/java/ru/practicum/stat/dto/ViewStatsDto.java new file mode 100644 index 0000000..75e35e9 --- /dev/null +++ b/stats/stat-dto/src/main/java/ru/practicum/stat/dto/ViewStatsDto.java @@ -0,0 +1,16 @@ +package ru.practicum.stat.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ViewStatsDto { + private String app; + private String uri; + private Long hits; +} diff --git a/stats/stats-server/pom.xml b/stats/stats-server/pom.xml new file mode 100644 index 0000000..6d5223e --- /dev/null +++ b/stats/stats-server/pom.xml @@ -0,0 +1,102 @@ + + + 4.0.0 + + ru.practicum + stats + 0.0.1-SNAPSHOT + + + stats-server + jar + + + + ru.practicum + stat-dto + 0.0.1-SNAPSHOT + + + org.projectlombok + lombok + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-validation + + + org.postgresql + postgresql + runtime + + + com.h2database + h2 + runtime + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.cloud + spring-cloud-starter-config + + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + com.github.ben-manes.caffeine + caffeine + + + + org.springframework.retry + spring-retry + + + org.springframework.boot + spring-boot-starter-aop + + + + org.springframework.boot + spring-boot-starter-actuator + + + io.micrometer + micrometer-registry-prometheus + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/stats/stats-server/src/main/java/ru/practicum/stat/server/StatServerApp.java b/stats/stats-server/src/main/java/ru/practicum/stat/server/StatServerApp.java new file mode 100644 index 0000000..9a0911f --- /dev/null +++ b/stats/stats-server/src/main/java/ru/practicum/stat/server/StatServerApp.java @@ -0,0 +1,11 @@ +package ru.practicum.stat.server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class StatServerApp { + public static void main(String[] args) { + SpringApplication.run(StatServerApp.class, args); + } +} diff --git a/stats/stats-server/src/main/java/ru/practicum/stat/server/controller/StatController.java b/stats/stats-server/src/main/java/ru/practicum/stat/server/controller/StatController.java new file mode 100644 index 0000000..dc37f5b --- /dev/null +++ b/stats/stats-server/src/main/java/ru/practicum/stat/server/controller/StatController.java @@ -0,0 +1,47 @@ +package ru.practicum.stat.server.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.stat.dto.EndpointHitDto; +import ru.practicum.stat.dto.ViewStatsDto; +import ru.practicum.stat.server.service.StatService; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@Slf4j +public class StatController { + + private final StatService statService; + + @PostMapping("/hit") + @ResponseStatus(HttpStatus.CREATED) + public void hit(@Valid @RequestBody EndpointHitDto endpointHitDto) { + log.info("Сохранение статистики: сервис={}, uri={}, ip={}", + endpointHitDto.getApp(), endpointHitDto.getUri(), endpointHitDto.getIp()); + statService.saveHit(endpointHitDto); + } + + @GetMapping("/stats") + public List getStats( + @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime start, + @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime end, + @RequestParam(required = false) List uris, + @RequestParam(defaultValue = "false") boolean unique + ) { + log.info("Запрос статистики: start={}, end={}, uris={}, unique={}", start, end, uris, unique); + + if (start.isAfter(end)) { + log.warn("Некорректный диапазон дат: start={} позже end={}", start, end); + throw new IllegalArgumentException("Дата окончания не может быть раньше даты начала"); + } + + return statService.getStats(start, end, uris, unique); + } +} diff --git a/stats/stats-server/src/main/java/ru/practicum/stat/server/dao/StatRepository.java b/stats/stats-server/src/main/java/ru/practicum/stat/server/dao/StatRepository.java new file mode 100644 index 0000000..c76d2dc --- /dev/null +++ b/stats/stats-server/src/main/java/ru/practicum/stat/server/dao/StatRepository.java @@ -0,0 +1,33 @@ +package ru.practicum.stat.server.dao; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import ru.practicum.stat.dto.ViewStatsDto; +import ru.practicum.stat.server.model.EndpointHit; + +import java.time.LocalDateTime; +import java.util.List; + +public interface StatRepository extends JpaRepository { + + @Query("SELECT new ru.practicum.stat.dto.ViewStatsDto(e.app, e.uri, COUNT(e.ip)) " + + "FROM EndpointHit e " + + "WHERE e.timestamp BETWEEN :start AND :end " + + "AND (:uris IS NULL OR e.uri IN :uris) " + + "GROUP BY e.app, e.uri " + + "ORDER BY COUNT(e.ip) DESC") + List getStats(@Param("start") LocalDateTime start, + @Param("end") LocalDateTime end, + @Param("uris") List uris); + + @Query("SELECT new ru.practicum.stat.dto.ViewStatsDto(e.app, e.uri, COUNT(DISTINCT e.ip)) " + + "FROM EndpointHit e " + + "WHERE e.timestamp BETWEEN :start AND :end " + + "AND (:uris IS NULL OR e.uri IN :uris) " + + "GROUP BY e.app, e.uri " + + "ORDER BY COUNT(DISTINCT e.ip) DESC") + List getStatsUnique(@Param("start") LocalDateTime start, + @Param("end") LocalDateTime end, + @Param("uris") List uris); +} diff --git a/stats/stats-server/src/main/java/ru/practicum/stat/server/mapper/EndpointHitMapper.java b/stats/stats-server/src/main/java/ru/practicum/stat/server/mapper/EndpointHitMapper.java new file mode 100644 index 0000000..fb45b19 --- /dev/null +++ b/stats/stats-server/src/main/java/ru/practicum/stat/server/mapper/EndpointHitMapper.java @@ -0,0 +1,17 @@ +package ru.practicum.stat.server.mapper; + +import org.springframework.lang.NonNull; +import ru.practicum.stat.dto.EndpointHitDto; +import ru.practicum.stat.server.model.EndpointHit; + +public class EndpointHitMapper { + + public static EndpointHit toEntity(@NonNull EndpointHitDto dto) { + return EndpointHit.builder() + .app(dto.getApp()) + .uri(dto.getUri()) + .ip(dto.getIp()) + .timestamp(dto.getTimestamp()) + .build(); + } +} diff --git a/stats/stats-server/src/main/java/ru/practicum/stat/server/model/EndpointHit.java b/stats/stats-server/src/main/java/ru/practicum/stat/server/model/EndpointHit.java new file mode 100644 index 0000000..d8181dc --- /dev/null +++ b/stats/stats-server/src/main/java/ru/practicum/stat/server/model/EndpointHit.java @@ -0,0 +1,31 @@ +package ru.practicum.stat.server.model; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "stats") +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class EndpointHit { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String app; + + @Column(nullable = false, length = 512) + private String uri; + + @Column(nullable = false, length = 45) + private String ip; + + @Column(name = "hit_timestamp", nullable = false) + private LocalDateTime timestamp; +} diff --git a/stats/stats-server/src/main/java/ru/practicum/stat/server/service/StatService.java b/stats/stats-server/src/main/java/ru/practicum/stat/server/service/StatService.java new file mode 100644 index 0000000..aaed2ce --- /dev/null +++ b/stats/stats-server/src/main/java/ru/practicum/stat/server/service/StatService.java @@ -0,0 +1,39 @@ +package ru.practicum.stat.server.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.stat.dto.EndpointHitDto; +import ru.practicum.stat.dto.ViewStatsDto; +import ru.practicum.stat.server.dao.StatRepository; +import ru.practicum.stat.server.mapper.EndpointHitMapper; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class StatService { + + private final StatRepository statRepository; + + @Transactional + public void saveHit(@NonNull EndpointHitDto endpointHitDto) { + log.debug("Сохранение записи о просмотре для URI: {}", endpointHitDto.getUri()); + statRepository.save(EndpointHitMapper.toEntity(endpointHitDto)); + } + + public List getStats(LocalDateTime start, LocalDateTime end, List uris, boolean unique) { + if (unique) { + log.debug("Получение статистики (уникальные IP) для URI: {}", uris); + return statRepository.getStatsUnique(start, end, uris); + } + + log.debug("Получение общей статистики для URI: {}", uris); + return statRepository.getStats(start, end, uris); + } +} diff --git a/stats/stats-server/src/main/java/ru/practicum/stat/server/util/config/StatServerConfiguration.java b/stats/stats-server/src/main/java/ru/practicum/stat/server/util/config/StatServerConfiguration.java new file mode 100644 index 0000000..669e26a --- /dev/null +++ b/stats/stats-server/src/main/java/ru/practicum/stat/server/util/config/StatServerConfiguration.java @@ -0,0 +1,28 @@ +package ru.practicum.stat.server.util.config; + +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; + +import java.time.format.DateTimeFormatter; + +@SpringBootConfiguration +public class StatServerConfiguration { + + @Bean + public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { + String pattern = "yyyy-MM-dd HH:mm:ss"; + return builder -> { + builder.simpleDateFormat(pattern); + builder.serializers(new LocalDateTimeSerializer( + DateTimeFormatter.ofPattern(pattern) + )); + builder.deserializers(new LocalDateTimeDeserializer( + DateTimeFormatter.ofPattern(pattern) + )); + }; + } + +} \ No newline at end of file diff --git a/stats/stats-server/src/main/java/ru/practicum/stat/server/util/error/ApiError.java b/stats/stats-server/src/main/java/ru/practicum/stat/server/util/error/ApiError.java new file mode 100644 index 0000000..d619347 --- /dev/null +++ b/stats/stats-server/src/main/java/ru/practicum/stat/server/util/error/ApiError.java @@ -0,0 +1,22 @@ +package ru.practicum.stat.server.util.error; + +import lombok.Getter; +import org.springframework.http.HttpStatus; +import java.time.LocalDateTime; + +@Getter +public class ApiError { + private HttpStatus status; + private String reason; + private String message; + private String errors; + private LocalDateTime timestamp = LocalDateTime.now(); + + public ApiError(HttpStatus status, String reason, String message, String stackTrace) { + this.status = status; + this.reason = reason; + this.message = message; + this.errors = stackTrace; + } +} + diff --git a/stats/stats-server/src/main/java/ru/practicum/stat/server/util/error/StatErrorHandler.java b/stats/stats-server/src/main/java/ru/practicum/stat/server/util/error/StatErrorHandler.java new file mode 100644 index 0000000..98e75d8 --- /dev/null +++ b/stats/stats-server/src/main/java/ru/practicum/stat/server/util/error/StatErrorHandler.java @@ -0,0 +1,48 @@ +package ru.practicum.stat.server.util.error; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import java.io.PrintWriter; +import java.io.StringWriter; + +@Slf4j +@RestControllerAdvice +public class StatErrorHandler { + + // Обработка 400 Bad Request + @ExceptionHandler({ + MethodArgumentNotValidException.class, + IllegalArgumentException.class, + org.springframework.web.bind.MissingServletRequestParameterException.class, // Отсутствие параметра + org.springframework.web.method.annotation.MethodArgumentTypeMismatchException.class // Неверный формат + }) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiError handleBadRequest(final Exception e) { // Здесь Exception + log.error("400 Bad Request: {}", e.getMessage()); + return new ApiError( + HttpStatus.BAD_REQUEST, + "Bad Request", + e.getMessage(), + getStackTrace(e) + ); + } + + private String getStackTrace(Exception e) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + return sw.toString(); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiError handleException(Exception e) { + log.info("500 {}", e.getMessage(), e); + String stackTrace = getStackTrace(e); + return new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, "Error ....", e.getMessage(), stackTrace); + } +} diff --git a/stats/stats-server/src/main/resources/application-test.yaml b/stats/stats-server/src/main/resources/application-test.yaml new file mode 100644 index 0000000..10090ac --- /dev/null +++ b/stats/stats-server/src/main/resources/application-test.yaml @@ -0,0 +1,26 @@ +spring: + config: + import: "optional:configserver:" + cloud: + config: + enabled: false + fail-fast: false + discovery: + enabled: false + application: + name: stats-server + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:ewm-stats + jpa: + hibernate: + ddl-auto: none + show-sql: true + +eureka: + client: + enabled: false + +logging: + level: + org.springframework.cloud: DEBUG \ No newline at end of file diff --git a/stats/stats-server/src/main/resources/application.yaml b/stats/stats-server/src/main/resources/application.yaml new file mode 100644 index 0000000..26f3ca3 --- /dev/null +++ b/stats/stats-server/src/main/resources/application.yaml @@ -0,0 +1,31 @@ +server: + port: 0 + +spring: + application: + name: stats-server + config: + import: "optional:configserver:" + cloud: + config: + discovery: + enabled: true + service-id: config-server + fail-fast: true + retry: + initial-interval: 1000 # начальная задержка (мс) + max-interval: 10000 # максимальная задержка (мс) + multiplier: 2.0 # множитель увеличения задержки + max-attempts: 20 # количество попыток (0 = бесконечно) + +eureka: + client: + serviceUrl: + defaultZone: http://localhost:8761/eureka/ + register-with-eureka: true + fetch-registry: true + instance: + prefer-ip-address: true + instance-id: ${spring.application.name}:${random.value} + lease-renewal-interval-in-seconds: 5 + lease-expiration-duration-in-seconds: 10 diff --git a/stats/stats-server/src/main/resources/schema.sql b/stats/stats-server/src/main/resources/schema.sql new file mode 100644 index 0000000..09ae1f7 --- /dev/null +++ b/stats/stats-server/src/main/resources/schema.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS stats ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + app VARCHAR(255) NOT NULL, + uri VARCHAR(512) NOT NULL, + ip VARCHAR(45) NOT NULL, + hit_timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL + ); \ No newline at end of file diff --git a/stats/stats-server/src/test/java/ru/practicum/stat/server/mapper/EndpointHitMapperTest.java b/stats/stats-server/src/test/java/ru/practicum/stat/server/mapper/EndpointHitMapperTest.java new file mode 100644 index 0000000..479ba67 --- /dev/null +++ b/stats/stats-server/src/test/java/ru/practicum/stat/server/mapper/EndpointHitMapperTest.java @@ -0,0 +1,30 @@ +package ru.practicum.stat.server.mapper; + +import org.junit.jupiter.api.Test; +import ru.practicum.stat.dto.EndpointHitDto; +import ru.practicum.stat.server.model.EndpointHit; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +class EndpointHitMapperTest { + + @Test + void toEntity_shouldMapAllFieldsCorrectly() { + LocalDateTime now = LocalDateTime.now(); + EndpointHitDto dto = EndpointHitDto.builder() + .app("ewm-main-service") + .uri("/events/1") + .ip("192.168.1.1") + .timestamp(now) + .build(); + EndpointHit entity = EndpointHitMapper.toEntity(dto); + + assertNotNull(entity); + assertEquals(dto.getApp(), entity.getApp()); + assertEquals(dto.getUri(), entity.getUri()); + assertEquals(dto.getIp(), entity.getIp()); + assertEquals(dto.getTimestamp(), entity.getTimestamp()); + } +} \ No newline at end of file