From 0fb0c05f9e7fc643d3ca2ea8930d180433be3f0d Mon Sep 17 00:00:00 2001 From: Sergey Filippovskikh <116564864+SergikF@users.noreply.github.com> Date: Thu, 17 Apr 2025 03:05:10 +0300 Subject: [PATCH 1/4] =?UTF-8?q?=D0=92=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=2015=20=D1=81=D0=BF=D1=80=D0=B8=D0=BD=D1=82?= =?UTF-8?q?=D0=B0.=20=D0=9F=D0=B5=D1=80=D0=B2=D0=B8=D1=87=D0=BD=D0=B0?= =?UTF-8?q?=D1=8F=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B0=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- er_diagram_shareIt.png | Bin 39148 -> 54320 bytes pom.xml | 6 - postman_for_shareit_15.json | 4811 +++++++++++++++++ .../ru/practicum/shareit/booking/Booking.java | 14 +- .../shareit/booking/BookingController.java | 88 +- .../practicum/shareit/booking/BookingDto.java | 27 +- .../shareit/booking/BookingDtoInput.java | 28 + .../shareit/booking/BookingDtoOutput.java | 30 + .../shareit/booking/BookingDtoShort.java | 24 + .../shareit/booking/BookingMapper.java | 65 + .../shareit/booking/BookingRepository.java | 32 + .../shareit/booking/BookingService.java | 51 + .../shareit/booking/BookingServiceImpl.java | 296 + .../shareit/booking/BookingState.java | 5 + .../ru/practicum/shareit/item/Comment.java | 36 + .../ru/practicum/shareit/item/CommentDto.java | 30 + .../shareit/item/CommentDtoOutput.java | 24 + .../shareit/item/CommentDtoShort.java | 24 + .../practicum/shareit/item/CommentMapper.java | 84 + .../shareit/item/CommentRepository.java | 9 + .../java/ru/practicum/shareit/item/Item.java | 25 +- .../shareit/item/ItemController.java | 34 +- .../ru/practicum/shareit/item/ItemDto.java | 13 +- .../practicum/shareit/item/ItemDtoOutput.java | 37 + .../practicum/shareit/item/ItemDtoShort.java | 20 + .../ru/practicum/shareit/item/ItemMapper.java | 164 +- .../practicum/shareit/item/ItemService.java | 3 + .../shareit/item/ItemServiceImpl.java | 75 +- .../{ItemRequest.java => Request.java} | 10 +- .../java/ru/practicum/shareit/user/User.java | 17 +- .../shareit/user/UserController.java | 16 +- .../ru/practicum/shareit/user/UserDto.java | 11 + .../practicum/shareit/user/UserDtoOutput.java | 29 + .../practicum/shareit/user/UserDtoShort.java | 18 + .../ru/practicum/shareit/user/UserMapper.java | 95 +- .../shareit/user/UserServiceImpl.java | 15 +- src/main/resources/application.properties | 16 - src/main/resources/application.yaml | 20 + src/main/resources/schema.sql | 62 +- 39 files changed, 6213 insertions(+), 151 deletions(-) create mode 100644 postman_for_shareit_15.json create mode 100644 src/main/java/ru/practicum/shareit/booking/BookingDtoInput.java create mode 100644 src/main/java/ru/practicum/shareit/booking/BookingDtoOutput.java create mode 100644 src/main/java/ru/practicum/shareit/booking/BookingDtoShort.java create mode 100644 src/main/java/ru/practicum/shareit/booking/BookingMapper.java create mode 100644 src/main/java/ru/practicum/shareit/booking/BookingRepository.java create mode 100644 src/main/java/ru/practicum/shareit/booking/BookingService.java create mode 100644 src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java create mode 100644 src/main/java/ru/practicum/shareit/booking/BookingState.java create mode 100644 src/main/java/ru/practicum/shareit/item/Comment.java create mode 100644 src/main/java/ru/practicum/shareit/item/CommentDto.java create mode 100644 src/main/java/ru/practicum/shareit/item/CommentDtoOutput.java create mode 100644 src/main/java/ru/practicum/shareit/item/CommentDtoShort.java create mode 100644 src/main/java/ru/practicum/shareit/item/CommentMapper.java create mode 100644 src/main/java/ru/practicum/shareit/item/CommentRepository.java create mode 100644 src/main/java/ru/practicum/shareit/item/ItemDtoOutput.java create mode 100644 src/main/java/ru/practicum/shareit/item/ItemDtoShort.java rename src/main/java/ru/practicum/shareit/request/{ItemRequest.java => Request.java} (71%) create mode 100644 src/main/java/ru/practicum/shareit/user/UserDtoOutput.java create mode 100644 src/main/java/ru/practicum/shareit/user/UserDtoShort.java delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yaml diff --git a/er_diagram_shareIt.png b/er_diagram_shareIt.png index 861fba7b6a997b2caf424dc714f24e3bbc1cc72d..ffa297d3f33b5101c3a3e47b99082a48472ca4a5 100644 GIT binary patch literal 54320 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 bi3MRRfA?T>D|#2b literal 39148 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 diff --git a/pom.xml b/pom.xml index fa8a467..2185679 100644 --- a/pom.xml +++ b/pom.xml @@ -37,7 +37,6 @@ org.postgresql postgresql - runtime @@ -57,11 +56,6 @@ spring-boot-starter-validation - - com.h2database - h2 - - org.springframework.boot spring-boot-starter-data-jpa diff --git a/postman_for_shareit_15.json b/postman_for_shareit_15.json new file mode 100644 index 0000000..26845fe --- /dev/null +++ b/postman_for_shareit_15.json @@ -0,0 +1,4811 @@ +{ + "info": { + "_postman_id": "069d256e-037e-4bd9-a5de-be6910726adc", + "name": "Sprint 15 ShareIt (add-bookings)", + "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-069d256e-037e-4bd9-a5de-be6910726adc?action=share&source=collection_link&creator=23073145" + }, + "item": [ + { + "name": "users", + "item": [ + { + "name": "Create user", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = rnd.getUser();\r", + " pm.collectionVariables.set(\"userName\", user.name);\r", + " pm.collectionVariables.set(\"userEmail\", user.email);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200 or 201\", function () {\r", + " pm.expect(pm.response.code).to.be.oneOf([200,201]);\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{userName}}\",\n \"email\": \"{{userEmail}}\"\n}" + }, + "url": { + "raw": "localhost:8080/users", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users" + ] + } + }, + "response": [] + }, + { + "name": "Create user without email", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user2 = rnd.getUser();\r", + " pm.collectionVariables.set(\"userName\", user2.name);\r", + " pm.collectionVariables.set(\"userEmail\", user2.email);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {\r", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{userName}}\"\n}" + }, + "url": { + "raw": "localhost:8080/users", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users" + ] + } + }, + "response": [] + }, + { + "name": "Create 2 users with same email", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user1 = rnd.getUser();\r", + " us = await api.addUser(user1);\r", + " user2 = rnd.getUser();\r", + " user2.email = user1.email;\r", + " pm.collectionVariables.set(\"userName\", user2.name);\r", + " pm.collectionVariables.set(\"userEmail\", user2.email);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 409\", function () {\r", + " pm.expect(pm.response.code).to.be.oneOf([409, 500]);\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{userName}}\",\n \"email\": \"{{userEmail}}\"\n}" + }, + "url": { + "raw": "localhost:8080/users", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users" + ] + } + }, + "response": [] + }, + { + "name": "Create user with invalid email", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = rnd.getUser();\r", + " pm.collectionVariables.set(\"userName\", user.name);\r", + " pm.collectionVariables.set(\"userEmail\", user.email);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {\r", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{userName}}\",\n \"email\": \"user.com\"\n}" + }, + "url": { + "raw": "localhost:8080/users", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users" + ] + } + }, + "response": [] + }, + { + "name": "User update", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = rnd.getUser();\r", + " us = await api.addUser(user);\r", + " pm.collectionVariables.set(\"userId\", us.id);\r", + " us = rnd.getUser()\r", + " pm.collectionVariables.set(\"userName\", us.name);\r", + " pm.collectionVariables.set(\"userEmail\", us.email);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.be.ok;\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "pm.test(\"Test user 'id' field\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('id');\r", + " var id = pm.collectionVariables.get(\"userId\");\r", + " pm.expect(jsonData.id, '\"id\" must be ' + id).to.eql(Number(id));\r", + "});\r", + "pm.test(\"Test user 'email' field\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('email');\r", + " var email = pm.collectionVariables.get(\"userEmail\");\r", + " pm.expect(jsonData.email, '\"email\" must be ' + email).to.eql(email);\r", + "});\r", + "pm.test(\"Test user 'name' field\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('name');\r", + " var name = pm.collectionVariables.get(\"userName\");\r", + " pm.expect(jsonData.name, '\"name\" must be ' + name).to.eql(name);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{userName}}\",\n \"email\": \"{{userEmail}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/users/{{userId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "{{userId}}" + ] + } + }, + "response": [] + }, + { + "name": "User update name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = rnd.getUser();\r", + " us = await api.addUser(user);\r", + " pm.collectionVariables.set(\"userId\", us.id);\r", + " us = rnd.getUser()\r", + " pm.collectionVariables.set(\"userName\", us.name);\r", + " pm.collectionVariables.set(\"userEmail\", us.email);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.be.ok;\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "pm.test(\"Test user 'id' field\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('id');\r", + " var id = pm.collectionVariables.get(\"userId\");\r", + " pm.expect(jsonData.id, '\"id\" must be ' + id).to.eql(Number(id));\r", + "});\r", + "pm.test(\"Test user 'name' field\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('name');\r", + " var name = pm.collectionVariables.get(\"userName\");\r", + " pm.expect(jsonData.name, '\"name\" must be ' + name).to.eql(name);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{userName}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/users/{{userId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "{{userId}}" + ] + } + }, + "response": [] + }, + { + "name": "User update email", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = rnd.getUser();\r", + " us = await api.addUser(user);\r", + " pm.collectionVariables.set(\"userId\", us.id);\r", + " us = rnd.getUser()\r", + " pm.collectionVariables.set(\"userName\", us.name);\r", + " pm.collectionVariables.set(\"userEmail\", us.email);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.be.ok;\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "pm.test(\"Test user 'id' field\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('id');\r", + " var id = pm.collectionVariables.get(\"userId\");\r", + " pm.expect(jsonData.id, '\"id\" must be ' + id).to.eql(Number(id));\r", + "});\r", + "pm.test(\"Test user 'email' field\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('email');\r", + " var email = pm.collectionVariables.get(\"userEmail\");\r", + " pm.expect(jsonData.email, '\"email\" must be ' + email).to.eql(email);\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{userEmail}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/users/{{userId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "{{userId}}" + ] + } + }, + "response": [] + }, + { + "name": "User update with existing email", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = rnd.getUser();\r", + " us = await api.addUser(user);\r", + " user2 = rnd.getUser();\r", + " us2 = await api.addUser(user2)\r", + " pm.collectionVariables.set(\"userId\", us2.id);\r", + " usa = rnd.getUser()\r", + " pm.collectionVariables.set(\"userName\", usa.name);\r", + " pm.collectionVariables.set(\"userEmail\", user.email);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 409\", function () {\r", + " pm.expect(pm.response.code).to.be.oneOf([409, 500]);\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{userName}}\",\n \"email\": \"{{userEmail}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/users/{{userId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "{{userId}}" + ] + } + }, + "response": [] + }, + { + "name": "Get user", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = rnd.getUser();\r", + " us = await api.addUser(user);\r", + " pm.collectionVariables.set(\"userId\", us.id);\r", + " pm.collectionVariables.set(\"userName\", us.name);\r", + " pm.collectionVariables.set(\"userEmail\", us.email);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.be.ok;\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "pm.test(\"Test user 'id' field\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('id');\r", + " var id = pm.collectionVariables.get(\"userId\");\r", + " pm.expect(jsonData.id, '\"id\" must be ' + id).to.eql(Number(id));\r", + "});\r", + "pm.test(\"Test user 'email' field\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('email');\r", + " var email = pm.collectionVariables.get(\"userEmail\");\r", + " pm.expect(jsonData.email, '\"email\" must be ' + email).to.eql(email);\r", + "});\r", + "pm.test(\"Test user 'name' field\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('name');\r", + " var name = pm.collectionVariables.get(\"userName\");\r", + " pm.expect(jsonData.name, '\"name\" must be ' + name).to.eql(name);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{baseUrl}}/users/{{userId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "{{userId}}" + ] + } + }, + "response": [] + }, + { + "name": "User delete", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([200,204]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = rnd.getUser();\r", + " us = await api.addUser(user);\r", + " pm.collectionVariables.set(\"userId\", us.id);\r", + " pm.collectionVariables.set(\"userName\", us.name);\r", + " pm.collectionVariables.set(\"userEmail\", us.email);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/users/{{userId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "{{userId}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "items", + "item": [ + { + "name": "Item create", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " item = rnd.getItem();\r", + " pm.collectionVariables.set(\"item\", item);\r", + " pm.collectionVariables.set(\"itemName\", item.name);\r", + " pm.collectionVariables.set(\"itemAvailable\", item.available);\r", + " pm.collectionVariables.set(\"itemDescription\", item.description);\r", + " user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200 or 201\", function () {\r", + " pm.expect(pm.response.code).to.be.oneOf([200,201]);\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "var item = pm.collectionVariables.get(\"item\");\r", + "\r", + "pm.test(\"Response data equal to request\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('id');\r", + " pm.expect(jsonData).to.have.property('name');\r", + " pm.expect(jsonData).to.have.property('description');\r", + " pm.expect(jsonData).to.have.property('available');\r", + " pm.expect(jsonData.name, `\"name\" must be ${item.name}`).to.eql(item.name);\r", + " pm.expect(jsonData.description, `\"description\" must be ${item.description}`).to.eql(item.description);\r", + " pm.expect(jsonData.available.toString(), `\"available\" must be ${item.available}`).to.eql(item.available.toString());\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{itemName}}\",\n \"description\": \"{{itemDescription}}\",\n \"available\": {{itemAvailable}}\n}" + }, + "url": { + "raw": "localhost:8080/items", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "items" + ] + } + }, + "response": [] + }, + { + "name": "Item create without X-Sharer-User-Id", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " item = rnd.getItem();\r", + " pm.collectionVariables.set(\"item\", item);\r", + " pm.collectionVariables.set(\"itemName\", item.name);\r", + " pm.collectionVariables.set(\"itemAvailable\", item.available);\r", + " pm.collectionVariables.set(\"itemDescription\", item.description);\r", + " user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400 or 500\", function () {\r", + " pm.expect(pm.response.code).to.be.oneOf([500, 400]);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "disabled": true + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{itemName}}\",\n \"description\": \"{{itemDescription}}\",\n \"available\": {{itemAvailable}}\n}" + }, + "url": { + "raw": "localhost:8080/items", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "items" + ] + } + }, + "response": [] + }, + { + "name": "Item create with non-existent user", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " item = rnd.getItem();\r", + " pm.collectionVariables.set(\"item\", item);\r", + " pm.collectionVariables.set(\"itemName\", item.name);\r", + " pm.collectionVariables.set(\"itemAvailable\", item.available);\r", + " pm.collectionVariables.set(\"itemDescription\", item.description);\r", + " user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user.id + '1');\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 404\", function () {\r", + " pm.expect(pm.response.code).to.be.oneOf([404]);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{itemName}}\",\n \"description\": \"{{itemDescription}}\",\n \"available\": {{itemAvailable}}\n}" + }, + "url": { + "raw": "localhost:8080/items", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "items" + ] + } + }, + "response": [] + }, + { + "name": "Item create without available field", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " item = rnd.getItem();\r", + " pm.collectionVariables.set(\"item\", item);\r", + " pm.collectionVariables.set(\"itemName\", item.name);\r", + " pm.collectionVariables.set(\"itemAvailable\", item.available);\r", + " pm.collectionVariables.set(\"itemDescription\", item.description);\r", + " user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {\r", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{itemName}}\",\n \"description\": \"{{itemDescription}}\"\n}" + }, + "url": { + "raw": "localhost:8080/items", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "items" + ] + } + }, + "response": [] + }, + { + "name": "Item create with empty name field", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"\",\n \"description\": \"Аккумуляторная отвертка\",\n \"available\": true\n}" + }, + "url": { + "raw": "{{baseUrl}}/items", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "items" + ] + } + }, + "response": [] + }, + { + "name": "Item create with empty description field", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Отвертка\",\n \"available\": true\n}" + }, + "url": { + "raw": "{{baseUrl}}/items", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "items" + ] + } + }, + "response": [] + }, + { + "name": "Item update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.be.ok;", + "});", + "pm.test(\"Response have body\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "var item = pm.collectionVariables.get(\"item\");", + "", + "pm.test(\"Response data equal to request\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('id');", + " pm.expect(jsonData).to.have.property('name');", + " pm.expect(jsonData).to.have.property('description');", + " pm.expect(jsonData).to.have.property('available');", + " pm.expect(jsonData.name, `\"name\" must be ${item.name}`).to.eql(item.name);", + " pm.expect(jsonData.description, `\"description\" must be ${item.description}`).to.eql(item.description);", + " pm.expect(jsonData.available.toString(), `\"available\" must be ${item.available}`).to.eql(item.available.toString());", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user.id);\r", + " item = rnd.getItem();\r", + " var it = await api.addItem(item, user.id)\r", + " item = rnd.getItem();\r", + " pm.collectionVariables.set(\"item\", item); \r", + " pm.collectionVariables.set(\"itemId\", it.id);\r", + " pm.collectionVariables.set(\"itemName\", item.name);\r", + " pm.collectionVariables.set(\"itemAvailable\", item.available);\r", + " pm.collectionVariables.set(\"itemDescription\", item.description);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{itemName}}\",\n \"description\": \"{{itemDescription}}\",\n \"available\": {{itemAvailable}}\n}" + }, + "url": { + "raw": "{{baseUrl}}/items/{{itemId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "items", + "{{itemId}}" + ] + } + }, + "response": [] + }, + { + "name": "Item update without X-Sharer-User-Id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 500\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([500, 400]);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user.id);\r", + " item = rnd.getItem();\r", + " var it = await api.addItem(item, user.id)\r", + " item = rnd.getItem();\r", + " pm.collectionVariables.set(\"item\", item); \r", + " pm.collectionVariables.set(\"itemId\", it.id);\r", + " pm.collectionVariables.set(\"itemName\", item.name);\r", + " pm.collectionVariables.set(\"itemAvailable\", item.available);\r", + " pm.collectionVariables.set(\"itemDescription\", item.description);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text", + "disabled": true + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{itemName}}\",\n \"description\": \"{{itemDescription}}\",\n \"available\": {{itemAvailable}}\n}" + }, + "url": { + "raw": "{{baseUrl}}/items/{{itemId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "items", + "{{itemId}}" + ] + } + }, + "response": [] + }, + { + "name": "Item update with other user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 404\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([404, 403]);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user.id + 1);\r", + " item = rnd.getItem();\r", + " var it = await api.addItem(item, user.id)\r", + " item = rnd.getItem();\r", + " pm.collectionVariables.set(\"item\", item); \r", + " pm.collectionVariables.set(\"itemId\", it.id);\r", + " pm.collectionVariables.set(\"itemName\", item.name);\r", + " pm.collectionVariables.set(\"itemAvailable\", item.available);\r", + " pm.collectionVariables.set(\"itemDescription\", item.description);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{itemName}}\",\n \"description\": \"{{itemDescription}}\",\n \"available\": {{itemAvailable}}\n}" + }, + "url": { + "raw": "{{baseUrl}}/items/{{itemId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "items", + "{{itemId}}" + ] + } + }, + "response": [] + }, + { + "name": "Item update available field", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.be.ok;", + "});", + "pm.test(\"Response have body\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "var item = pm.collectionVariables.get(\"item\");", + "", + "pm.test(\"Response data equal to request\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('id');", + " pm.expect(jsonData).to.have.property('available');", + " pm.expect(jsonData.available.toString(), `\"available\" must be ${item.available}`).to.eql(item.available.toString());", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user.id);\r", + " item = rnd.getItem();\r", + " var it = await api.addItem(item, user.id)\r", + " item = rnd.getItem();\r", + " pm.collectionVariables.set(\"item\", item); \r", + " pm.collectionVariables.set(\"itemId\", it.id);\r", + " pm.collectionVariables.set(\"itemName\", item.name);\r", + " pm.collectionVariables.set(\"itemAvailable\", item.available);\r", + " pm.collectionVariables.set(\"itemDescription\", item.description);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"available\": {{itemAvailable}}\n}" + }, + "url": { + "raw": "{{baseUrl}}/items/{{itemId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "items", + "{{itemId}}" + ] + } + }, + "response": [] + }, + { + "name": "Item update description field", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.be.ok;", + "});", + "pm.test(\"Response have body\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "var item = pm.collectionVariables.get(\"item\");", + "", + "pm.test(\"Response data equal to request\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('id');", + " pm.expect(jsonData).to.have.property('name');", + " pm.expect(jsonData).to.have.property('description');", + " pm.expect(jsonData).to.have.property('available');", + " pm.expect(jsonData.description, `\"description\" must be ${item.description}`).to.eql(item.description);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user.id);\r", + " item = rnd.getItem();\r", + " var it = await api.addItem(item, user.id)\r", + " item = rnd.getItem();\r", + " pm.collectionVariables.set(\"item\", item); \r", + " pm.collectionVariables.set(\"itemId\", it.id);\r", + " pm.collectionVariables.set(\"itemName\", item.name);\r", + " pm.collectionVariables.set(\"itemAvailable\", item.available);\r", + " pm.collectionVariables.set(\"itemDescription\", item.description);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"description\": \"{{itemDescription}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/items/{{itemId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "items", + "{{itemId}}" + ] + } + }, + "response": [] + }, + { + "name": "Item update name field", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.be.ok;", + "});", + "pm.test(\"Response have body\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "var item = pm.collectionVariables.get(\"item\");", + "", + "pm.test(\"Response data equal to request\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('id');", + " pm.expect(jsonData).to.have.property('name');", + " pm.expect(jsonData).to.have.property('description');", + " pm.expect(jsonData).to.have.property('available');", + " pm.expect(jsonData.name, `\"name\" must be ${item.name}`).to.eql(item.name);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user.id);\r", + " item = rnd.getItem();\r", + " var it = await api.addItem(item, user.id)\r", + " item = rnd.getItem();\r", + " pm.collectionVariables.set(\"item\", item); \r", + " pm.collectionVariables.set(\"itemId\", it.id);\r", + " pm.collectionVariables.set(\"itemName\", item.name);\r", + " pm.collectionVariables.set(\"itemAvailable\", item.available);\r", + " pm.collectionVariables.set(\"itemDescription\", item.description);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{itemName}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/items/{{itemId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "items", + "{{itemId}}" + ] + } + }, + "response": [] + }, + { + "name": "Item get", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.be.ok;", + "});", + "pm.test(\"Response have body\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "var item = pm.collectionVariables.get(\"item\");", + "", + "pm.test(\"Response data equal to request\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('id');", + " pm.expect(jsonData).to.have.property('name');", + " pm.expect(jsonData).to.have.property('description');", + " pm.expect(jsonData).to.have.property('available');", + " pm.expect(jsonData.name, `\"name\" must be ${item.name}`).to.eql(item.name);", + " pm.expect(jsonData.description, `\"description\" must be ${item.description}`).to.eql(item.description);", + " pm.expect(jsonData.available.toString(), `\"available\" must be ${item.available}`).to.eql(item.available.toString());", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user.id);\r", + " item = await api.addItem(rnd.getItem(), user.id)\r", + " pm.collectionVariables.set(\"item\", item);\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"itemName\", item.name);\r", + " pm.collectionVariables.set(\"itemAvailable\", item.available);\r", + " pm.collectionVariables.set(\"itemDescription\", item.description);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/items/{{itemId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "items", + "{{itemId}}" + ] + } + }, + "response": [] + }, + { + "name": "Get all items from user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.be.ok;", + "});", + "pm.test(\"Test list item response\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.length, 'List length must be 2').to.eql(2);", + "});", + "", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user.id);\r", + " await api.addItem(rnd.getItem(), user.id)\r", + " await api.addItem(rnd.getItem(), user.id)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/items", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "items" + ] + } + }, + "response": [] + }, + { + "name": "Item search", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.be.ok;", + "});", + "pm.test(\"Test list item response\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.length, 'List length must be 1').to.eql(1);", + " pm.expect(jsonData[0].name.toUpperCase(), 'Name should include ' + pm.collectionVariables.get(\"searchString\")).to.eql(pm.collectionVariables.get(\"searchString\"))", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user.id);\r", + " it = rnd.getItem()\r", + " it.available = true\r", + " item = await api.addItem(it, user.id)\r", + " pm.collectionVariables.set(\"item\", item);\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"itemName\", item.name);\r", + " pm.collectionVariables.set(\"searchString\", item.name.toUpperCase());\r", + " pm.collectionVariables.set(\"itemAvailable\", item.available);\r", + " pm.collectionVariables.set(\"itemDescription\", item.description);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/items/search?text={{searchString}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "items", + "search" + ], + "query": [ + { + "key": "text", + "value": "{{searchString}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Item search unavailable", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.be.ok;", + "});", + "pm.test(\"Test list item response\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.length, 'List length must be 0').to.eql(0);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user.id);\r", + " it = rnd.getItem()\r", + " it.available = false\r", + " item = await api.addItem(it, user.id)\r", + " pm.collectionVariables.set(\"item\", item);\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"itemName\", item.name);\r", + " pm.collectionVariables.set(\"searchString\", item.name.toUpperCase());\r", + " pm.collectionVariables.set(\"itemAvailable\", item.available);\r", + " pm.collectionVariables.set(\"itemDescription\", item.description);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/items/search?text={{searchString}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "items", + "search" + ], + "query": [ + { + "key": "text", + "value": "{{searchString}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Item search empty", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.be.ok;", + "});", + "pm.test(\"Test search item response\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.length, 'List length must be 0').to.eql(0);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/items/search?text=", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "items", + "search" + ], + "query": [ + { + "key": "text", + "value": "" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "bookings", + "item": [ + { + "name": "Booking unavailable item", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = false\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"itemId\": {{itemId}},\n \"start\": \"{{start}}\",\n \"end\": \"{{end}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/bookings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings" + ] + } + }, + "response": [] + }, + { + "name": "Booking create failed by wrong userId", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 500\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([500, 404, 403]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user2.id + 1);\r", + " it = rnd.getItem()\r", + " it.available = false\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"itemId\": {{itemId}},\n \"start\": \"{{start}}\",\n \"end\": \"{{end}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/bookings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings" + ] + } + }, + "response": [] + }, + { + "name": "Booking create failed by not found itemId", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 404\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([404]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = false\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id + 1);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"itemId\": {{itemId}},\n \"start\": \"{{start}}\",\n \"end\": \"{{end}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/bookings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings" + ] + } + }, + "response": [] + }, + { + "name": "Booking create failed by end in past", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().subtract(3, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().subtract(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = false\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"itemId\": {{itemId}},\n \"start\": \"{{start}}\",\n \"end\": \"{{end}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/bookings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings" + ] + } + }, + "response": [] + }, + { + "name": "Booking create failed by start equal end", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = false\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"itemId\": {{itemId}},\n \"start\": \"{{start}}\",\n \"end\": \"{{end}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/bookings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings" + ] + } + }, + "response": [] + }, + { + "name": "Booking create failed by start equal null", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "var moment = require('moment');\r", + "\r", + "var start = moment().add(2, 'd');\r", + "pm.environment.set('start_null', start.format('YYYY-MM-DDTHH:mm:ss'));\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"itemId\": {{itemId}},\n \"end\": \"{{end}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/bookings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings" + ] + } + }, + "response": [] + }, + { + "name": "Booking create failed by end equal null", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "var moment = require('moment');\r", + "\r", + "var start = moment().add(2, 'd');\r", + "pm.environment.set('end_null', start.format('YYYY-MM-DDTHH:mm:ss'));\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"itemId\": {{itemId}},\n \"start\": \"{{start}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/bookings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings" + ] + } + }, + "response": [] + }, + { + "name": "Booking create failed by start in past", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().subtract(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = false\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"itemId\": {{itemId}},\n \"start\": \"{{start}}\",\n \"end\": \"{{end}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/bookings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings" + ] + } + }, + "response": [] + }, + { + "name": "Booking available item", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200 or 201\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([200,201]);", + "});", + "pm.test(\"Has booking create response\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "var item = pm.collectionVariables.get('item')", + "var user1 = pm.collectionVariables.get('user1')", + "var user2 = pm.collectionVariables.get('user2')", + "var start = pm.collectionVariables.get('start')", + "var end = pm.collectionVariables.get('end')", + "pm.test(\"Test booking 'start' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('start');", + " pm.expect(jsonData.start, '\"start\" must be \"' + pm.collectionVariables.get('start') + '\"').to.eql(pm.collectionVariables.get('start'));", + "});", + "pm.test(\"Test booking 'end' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('end');", + " pm.expect(jsonData.end, '\"end\" must be \"' + pm.collectionVariables.get('end') + '\"').to.eql(pm.collectionVariables.get('end'));", + "});", + "pm.test(\"Test booking 'status' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('status');", + " pm.expect(jsonData.status, '\"status\" must be \"WAITING\"').to.eql('WAITING');", + "});", + "pm.test(\"Test booking 'booker.id' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('booker');", + " pm.expect(jsonData.booker).to.have.property('id');", + " pm.expect(jsonData.booker.id, '\"booker.id\" must be ${user2.id}').to.eql(user2.id);", + "});", + "pm.test(\"Test booking 'item.id' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('item');", + " pm.expect(jsonData.item).to.have.property('id');", + " pm.expect(jsonData.item.id, '\"item.id\" must be ${item.id}').to.eql(item.id);", + "});", + "pm.test(\"Test booking 'item.name' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('item');", + " pm.expect(jsonData.item).to.have.property('name');", + " pm.expect(jsonData.item.name, '\"item.name\" must be ${item.name}').to.eql(item.name);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user1\", user1);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user2\", user2);\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = true\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"item\", item);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"itemId\": {{itemId}},\n \"start\": \"{{start}}\",\n \"end\": \"{{end}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/bookings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings" + ] + } + }, + "response": [] + }, + { + "name": "Booking approve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([200]);", + "});", + "pm.test(\"Has booking patch response\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "var item = pm.collectionVariables.get('item')", + "var book = pm.collectionVariables.get('booking')", + "var user1 = pm.collectionVariables.get('user1')", + "var user2 = pm.collectionVariables.get('user2')", + "pm.test(\"Test booking 'id' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('id');", + " pm.expect(jsonData.id, '\"id\" must be ${book.id}').to.eql(book.id);", + "});", + "pm.test(\"Test booking 'start' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('start');", + " pm.expect(jsonData.start, '\"start\" must be \"' + pm.collectionVariables.get('start') + '\"').to.eql(pm.collectionVariables.get('start'));", + "});", + "pm.test(\"Test booking 'end' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('end');", + " pm.expect(jsonData.end, '\"end\" must be \"' + pm.collectionVariables.get('end') + '\"').to.eql(pm.collectionVariables.get('end'));", + "});", + "pm.test(\"Test booking 'status' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('status');", + " pm.expect(jsonData.status, '\"status\" must be \"APPROVED\"').to.eql('APPROVED');", + "});", + "pm.test(\"Test booking 'booker.id' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('booker');", + " pm.expect(jsonData.booker).to.have.property('id');", + " pm.expect(jsonData.booker.id, '\"booker.id\" must be ${user2.id}').to.eql(user2.id);", + "});", + "pm.test(\"Test booking 'item.id' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('item');", + " pm.expect(jsonData.item).to.have.property('id');", + " pm.expect(jsonData.item.id, '\"item.id\" must be ${item.id}').to.eql(item.id);", + "});", + "pm.test(\"Test booking 'item.name' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('item');", + " pm.expect(jsonData.item).to.have.property('name');", + " pm.expect(jsonData.item.name, '\"item.name\" must be ${item.name}').to.eql(item.name);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user1\", user1);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user2\", user2);\r", + " pm.collectionVariables.set(\"userId\", user1.id);\r", + " it = rnd.getItem()\r", + " it.available = true\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"item\", item);\r", + " book = await api.addBooking(rnd.getBooking(item.id, start, end), user2.id)\r", + " pm.collectionVariables.set(\"bookingId\", book.id);\r", + " pm.collectionVariables.set(\"booking\", book);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/bookings/{{bookingId}}?approved=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings", + "{{bookingId}}" + ], + "query": [ + { + "key": "approved", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "Booking approve by wrong user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 403\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([400, 403, 500]);", + "});", + "pm.test(\"Has booking patch response\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user1\", user1);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user2\", user2);\r", + " pm.collectionVariables.set(\"userId\", user1.id + 2);\r", + " it = rnd.getItem()\r", + " it.available = true\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"item\", item);\r", + " book = await api.addBooking(rnd.getBooking(item.id, start, end), user2.id)\r", + " pm.collectionVariables.set(\"bookingId\", book.id);\r", + " pm.collectionVariables.set(\"booking\", book);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/bookings/{{bookingId}}?approved=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings", + "{{bookingId}}" + ], + "query": [ + { + "key": "approved", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "Get booking by booker", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.be.ok;", + "});", + "pm.test(\"Has booking create response\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "var item = pm.collectionVariables.get('item')", + "var user1 = pm.collectionVariables.get('user1')", + "var user2 = pm.collectionVariables.get('user2')", + "var start = pm.collectionVariables.get('start')", + "var end = pm.collectionVariables.get('end')", + "pm.test(\"Test booking 'start' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('start');", + " pm.expect(jsonData.start, '\"start\" must be \"' + pm.collectionVariables.get('start') + '\"').to.eql(pm.collectionVariables.get('start'));", + "});", + "pm.test(\"Test booking 'end' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('end');", + " pm.expect(jsonData.end, '\"end\" must be \"' + pm.collectionVariables.get('end') + '\"').to.eql(pm.collectionVariables.get('end'));", + "});", + "pm.test(\"Test booking 'status' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('status');", + " pm.expect(jsonData.status, '\"status\" must be \"WAITING\"').to.eql('WAITING');", + "});", + "pm.test(\"Test booking 'booker.id' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('booker');", + " pm.expect(jsonData.booker).to.have.property('id');", + " pm.expect(jsonData.booker.id, '\"booker.id\" must be ${user2.id}').to.eql(user2.id);", + "});", + "pm.test(\"Test booking 'item.id' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('item');", + " pm.expect(jsonData.item).to.have.property('id');", + " pm.expect(jsonData.item.id, '\"item.id\" must be ${item.id}').to.eql(item.id);", + "});", + "pm.test(\"Test booking 'item.name' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('item');", + " pm.expect(jsonData.item).to.have.property('name');", + " pm.expect(jsonData.item.name, '\"item.name\" must be ${item.name}').to.eql(item.name);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user1\", user1);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user2\", user2);\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = true\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"item\", item);\r", + " book = await api.addBooking(rnd.getBooking(item.id, start, end), user2.id)\r", + " pm.collectionVariables.set(\"bookingId\", book.id);\r", + " pm.collectionVariables.set(\"booking\", book);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/bookings/{{bookingId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings", + "{{bookingId}}" + ] + } + }, + "response": [] + }, + { + "name": "Get booking by owner", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.be.ok;", + "});", + "pm.test(\"Has booking create response\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "var item = pm.collectionVariables.get('item')", + "var user1 = pm.collectionVariables.get('user1')", + "var user2 = pm.collectionVariables.get('user2')", + "var start = pm.collectionVariables.get('start')", + "var end = pm.collectionVariables.get('end')", + "pm.test(\"Test booking 'start' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('start');", + " pm.expect(jsonData.start, '\"start\" must be \"' + pm.collectionVariables.get('start') + '\"').to.eql(pm.collectionVariables.get('start'));", + "});", + "pm.test(\"Test booking 'end' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('end');", + " pm.expect(jsonData.end, '\"end\" must be \"' + pm.collectionVariables.get('end') + '\"').to.eql(pm.collectionVariables.get('end'));", + "});", + "pm.test(\"Test booking 'status' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('status');", + " pm.expect(jsonData.status, '\"status\" must be \"WAITING\"').to.eql('WAITING');", + "});", + "pm.test(\"Test booking 'booker.id' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('booker');", + " pm.expect(jsonData.booker).to.have.property('id');", + " pm.expect(jsonData.booker.id, '\"booker.id\" must be ${user2.id}').to.eql(user2.id);", + "});", + "pm.test(\"Test booking 'item.id' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('item');", + " pm.expect(jsonData.item).to.have.property('id');", + " pm.expect(jsonData.item.id, '\"item.id\" must be ${item.id}').to.eql(item.id);", + "});", + "pm.test(\"Test booking 'item.name' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('item');", + " pm.expect(jsonData.item).to.have.property('name');", + " pm.expect(jsonData.item.name, '\"item.name\" must be ${item.name}').to.eql(item.name);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user1\", user1);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user2\", user2);\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = true\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"item\", item);\r", + " book = await api.addBooking(rnd.getBooking(item.id, start, end), user2.id)\r", + " pm.collectionVariables.set(\"bookingId\", book.id);\r", + " pm.collectionVariables.set(\"booking\", book);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/bookings/{{bookingId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings", + "{{bookingId}}" + ] + } + }, + "response": [] + }, + { + "name": "Get all bookings from wrong user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 500\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([500,404,403]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user2.id + 1);\r", + "\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/bookings/owner", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings", + "owner" + ] + } + }, + "response": [] + }, + { + "name": "Get all user bookings", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.be.ok;", + "});", + "pm.test(\"Has booking patch response\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "var item = pm.collectionVariables.get('item')", + "var book = pm.collectionVariables.get('booking')", + "var user1 = pm.collectionVariables.get('user1')", + "var user2 = pm.collectionVariables.get('user2')", + "var jsonData = pm.response.json()[0];", + "", + "pm.test(\"Test booking 'id' field\", function () {", + " pm.expect(jsonData).to.have.property('id');", + " pm.expect(jsonData.id, '\"id\" must be ${book.id}').to.eql(book.id);", + "});", + "pm.test(\"Test booking 'start' field\", function () {", + " pm.expect(jsonData).to.have.property('start');", + " pm.expect(jsonData.start, '\"start\" must be \"' + pm.collectionVariables.get('start') + '\"').to.eql(pm.collectionVariables.get('start'));", + "});", + "pm.test(\"Test booking 'end' field\", function () {", + " pm.expect(jsonData).to.have.property('end');", + " pm.expect(jsonData.end, '\"end\" must be \"' + pm.collectionVariables.get('end') + '\"').to.eql(pm.collectionVariables.get('end'));", + "});", + "pm.test(\"Test booking 'status' field\", function () {", + " pm.expect(jsonData).to.have.property('status');", + " pm.expect(jsonData.status, '\"status\" must be \"WAITING\"').to.eql('WAITING');", + "});", + "pm.test(\"Test booking 'booker.id' field\", function () {", + " pm.expect(jsonData).to.have.property('booker');", + " pm.expect(jsonData.booker).to.have.property('id');", + " pm.expect(jsonData.booker.id, '\"booker.id\" must be ${user2.id}').to.eql(user2.id);", + "});", + "pm.test(\"Test booking 'item.id' field\", function () {", + " pm.expect(jsonData).to.have.property('item');", + " pm.expect(jsonData.item).to.have.property('id');", + " pm.expect(jsonData.item.id, '\"item.id\" must be ${item.id}').to.eql(item.id);", + "});", + "pm.test(\"Test booking 'item.name' field\", function () {", + " pm.expect(jsonData).to.have.property('item');", + " pm.expect(jsonData.item).to.have.property('name');", + " pm.expect(jsonData.item.name, '\"item.name\" must be ${item.name}').to.eql(item.name);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user1\", user1);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user2\", user2);\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = true\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"item\", item);\r", + " book = await api.addBooking(rnd.getBooking(item.id, start, end), user2.id)\r", + " pm.collectionVariables.set(\"bookingId\", book.id);\r", + " pm.collectionVariables.set(\"booking\", book);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/bookings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "comments", + "item": [ + { + "name": "Comment past booking", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200 or 201\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([200,201]);", + "});", + "pm.test(\"Response have body\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "var item = pm.collectionVariables.get(\"item\");", + "var user1 = pm.collectionVariables.get(\"user1\");", + "var user2 = pm.collectionVariables.get(\"user2\");", + "var text = pm.collectionVariables.get(\"commentText\")", + "pm.test(\"Response data equal to request\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('id');", + " pm.expect(jsonData).to.have.property('text');", + " pm.expect(jsonData).to.have.property('authorName');", + " pm.expect(jsonData).to.have.property('created');", + " pm.expect(jsonData.text, `\"text\" must be ` + text).to.eql(text);", + " pm.expect(jsonData.authorName, `\"authorName\" must be ${user2.name}`).to.eql(user2.name);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'seconds').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'seconds').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user1\", user1);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user2\", user2);\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = true\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"item\", item);\r", + " book = await api.addBooking(rnd.getBooking(item.id, start, end), user2.id)\r", + " await api.approveBooking(book.id, user1.id)\r", + " pm.collectionVariables.set(\"bookingId\", book.id);\r", + " pm.collectionVariables.set(\"booking\", book);\r", + " pm.collectionVariables.set(\"commentText\", rnd.getWord(50));\r", + " setTimeout(function(){}, 3000);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + }, + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"text\": \"{{commentText}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/items/{{itemId}}/comment", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "items", + "{{itemId}}", + "comment" + ] + } + }, + "response": [] + }, + { + "name": "Comment approved booking", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([400, 500])", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'seconds').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'seconds').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user1\", user1);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user2\", user2);\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = true\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"item\", item);\r", + " book = await api.addBooking(rnd.getBooking(item.id, start, end), user2.id)\r", + " await api.approveBooking(book.id, user1.id)\r", + " pm.collectionVariables.set(\"bookingId\", book.id);\r", + " pm.collectionVariables.set(\"booking\", book);\r", + " pm.collectionVariables.set(\"commentText\", rnd.getWord(50));\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + }, + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"text\": \"{{commentText}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/items/{{itemId}}/comment", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "items", + "{{itemId}}", + "comment" + ] + } + }, + "response": [] + }, + { + "name": "Get item with comments", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.be.ok;", + "});", + "pm.test(\"Has item create response\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "pm.test(\"Test item 'id' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('id');", + "});", + "pm.test(\"Test item 'name' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('name');", + "});", + "pm.test(\"Test item 'description' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('description');", + "});", + "pm.test(\"Test item 'available' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('available');", + "});", + "pm.test(\"Test item 'lastBooking' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('lastBooking');", + " pm.expect(jsonData.lastBooking, '\"lastBooking\" must be \"null\"').null;", + "});", + "pm.test(\"Test item 'nextBooking' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('nextBooking');", + " pm.expect(jsonData.nextBooking, '\"nextBooking\" must be \"null\"').null;", + "});", + "pm.test(\"Test item 'comments' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('comments');", + " pm.expect(jsonData.comments.length, 'length of \"comments\" must be \"1\"').to.eql(1);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'seconds').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'seconds').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user1\", user1);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user2\", user2);\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = true\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"item\", item);\r", + " book = await api.addBooking(rnd.getBooking(item.id, start, end), user2.id)\r", + " await api.approveBooking(book.id, user1.id)\r", + " pm.collectionVariables.set(\"bookingId\", book.id);\r", + " pm.collectionVariables.set(\"booking\", book);\r", + " pm.collectionVariables.set(\"commentText\", rnd.getWord(50));\r", + " pausecomp(3000);\r", + " await api.getBooking(book.id, user1.id)\r", + " comment = await api.addComment({\"text\": rnd.getWord(50)}, item.id, user2.id)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "function pausecomp(millis)\r", + " {\r", + " var date = new Date();\r", + " var curDate = null;\r", + " do { curDate = new Date(); }\r", + " while(curDate-date < millis);\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": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/items/{{itemId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "items", + "{{itemId}}" + ] + } + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "API = class {\r", + " constructor(postman, verbose = false, baseUrl = \"http://localhost:8080\") {\r", + " this.baseUrl = baseUrl;\r", + " this.pm = postman;\r", + " this._verbose = verbose;\r", + " }\r", + "\r", + " async addUser(user, id=0, verbose=null) {\r", + " return this.post(\"/users\", user, id, \"Ошибка при добавлении нового пользователя: \", verbose);\r", + " }\r", + "\r", + " async addItem(item, id=0, verbose=null) {\r", + " return this.post(\"/items\", item, id, \"Ошибка при добавлении новой вещи: \", verbose);\r", + " }\r", + "\r", + " async addBooking(booking, id=0, verbose=null) {\r", + " return this.post(\"/bookings\", booking, id, \"Ошибка при добавлении нового бронирования: \", verbose);\r", + " }\r", + "\r", + " async addComment(comment, itemId, id=0, verbose=null) {\r", + " return this.post(\"/items/\" + itemId + \"/comment\", comment, id, \"Ошибка при добавлении нового комментария: \", verbose);\r", + " }\r", + "\r", + " async getBooking(bookingId, id=0, verbose=null) {\r", + " return this.get(\"/bookings/\"+bookingId, {}, id, \"Ошибка при получении информации о бронировании: \", verbose);\r", + " }\r", + "\r", + " async approveBooking(bookingId, id=0, verbose=null) {\r", + " return this.patch(\"/bookings/\"+bookingId+\"?approved=true\", {}, id, \"Ошибка при подтверждении бронирования: \", verbose);\r", + " }\r", + "\r", + " async addRequest(request, id=0, verbose=null) {\r", + " return this.post(\"/requests\", request, id, \"Ошибка при добавлении нового запроса: \", verbose);\r", + " }\r", + " \r", + " async post(path, body, id=0, errorText = \"Ошибка при выполнении post-запроса: \", verbose=null) {\r", + " return this.sendRequest(\"POST\", path, body, id, errorText, verbose);\r", + " }\r", + "\r", + " async patch(path, body = null, id=0, errorText = \"Ошибка при выполнении patch-запроса: \", verbose=null) {\r", + " return this.sendRequest(\"PATCH\", path, body, id, errorText, verbose);\r", + " }\r", + "\r", + " async get(path, body = null, id=0, errorText = \"Ошибка при выполнении get-запроса: \", verbose=null) {\r", + " return this.sendRequest(\"GET\", path, body, id, errorText, verbose);\r", + " }\r", + "\r", + " async put(path, body = null, id=0, errorText = \"Ошибка при выполнении put-запроса: \", verbose=null) {\r", + " return this.sendRequest(\"PUT\", path, body, id, errorText, verbose);\r", + " }\r", + "\r", + " async delete(path, body = null, id=0, errorText = \"Ошибка при выполнении delte-запроса: \", verbose=null) {\r", + " return this.sendRequest(\"DELETE\", path, body, id, errorText, verbose);\r", + " }\r", + "\r", + " async sendRequest(method, path, body=null, id=0, errorText = \"Ошибка при выполнении запроса: \", verbose=null) {\r", + " return new Promise((resolve, reject) => {\r", + " verbose = verbose == null ? this._verbose : verbose;\r", + " var req = {};\r", + " if (id == 0){\r", + " req = {\r", + " url: this.baseUrl + path,\r", + " method: method,\r", + " body: body == null ? \"\" : JSON.stringify(body),\r", + " header: { \"Content-Type\": \"application/json\"},\r", + " };\r", + " }else{\r", + " req = {\r", + " url: this.baseUrl + path,\r", + " method: method,\r", + " body: body == null ? \"\" : JSON.stringify(body),\r", + " header: [{\r", + " \"key\": \"X-Sharer-User-Id\",\r", + " \"value\": id,\r", + " \"type\": \"text\",\r", + " },\r", + " {\r", + " \"key\": \"Content-Type\",\r", + " \"name\": \"Content-Type\",\r", + " \"value\": \"application/json\",\r", + " \"type\": \"text\"\r", + " }]\r", + " };\r", + " }\r", + " if(verbose) {\r", + " console.log(\"Отправляю запрос: \", req);\r", + " }\r", + "\r", + " try {\r", + " this.pm.sendRequest(req, (error, response) => {\r", + " if(error || (response.code >= 400 && response.code <= 599)) {\r", + " let err = error ? error : JSON.stringify(response.json());\r", + " console.error(\"При выполнении запроса к серверу возникла ошибка.\\n\", err,\r", + " \"\\nДля отладки проблемы повторите такой же запрос к вашей программе \" + \r", + " \"на локальном компьютере. Данные запроса:\\n\", JSON.stringify(request));\r", + "\r", + " reject(new Error(errorText + err));\r", + " }\r", + " if(verbose) {\r", + " console.log(\"Результат обработки запроса: код состояния - \", response.code, \", тело: \", response.json());\r", + " }\r", + " if (response.stream.length === 0){\r", + " resolve(null);\r", + " }else{\r", + " resolve(response.json());\r", + " }\r", + " });\r", + " \r", + " } catch(err) {\r", + " if(verbose) {\r", + " console.error(errorText, err);\r", + " }\r", + " return Promise.reject(err);\r", + " }\r", + " });\r", + " }\r", + "};\r", + "\r", + "RandomUtils = class {\r", + " constructor() {}\r", + "\r", + " getUser() {\r", + " return {\r", + " name: pm.variables.replaceIn('{{$randomFullName}}'),\r", + " email: pm.variables.replaceIn('{{$randomEmail}}'),\r", + " };\r", + " }\r", + "\r", + " getRequest() {\r", + " return {\r", + " description: this.getWord(50)\r", + " };\r", + " }\r", + "\r", + " getBooking(id, startBook, endBook) {\r", + " return {\r", + " itemId: id, \r", + " start: startBook,\r", + " end: endBook \r", + " };\r", + " }\r", + "\r", + " getItem() {\r", + " return {\r", + " name: this.getWord(10),\r", + " description: this.getWord(50),\r", + " available: pm.variables.replaceIn('{{$randomBoolean}}')\t\r", + " };\r", + " }\r", + "\r", + " getItemForRequest(id) {\r", + " return {\r", + " name: this.getWord(10),\r", + " description: this.getWord(50),\r", + " available: pm.variables.replaceIn('{{$randomBoolean}}'),\r", + " requestId: id\r", + " };\r", + " }\r", + "\r", + " getFilm(director=null) {\r", + " let date = new Date(new Date(1960, 0, 1).getTime() + Math.random() * (new Date(2010, 0, 1).getTime() - new Date(1960, 0, 1).getTime()));\r", + " var toReturn = {\r", + " name: this.getWord(15),\r", + " description: this.getWord(50),\r", + " releaseDate: date.toISOString().slice(0,10),\r", + " duration: Math.floor(Math.random() * (180 - 60 + 1) + 60),\r", + " mpa: { id: Math.floor(Math.random() * (5 - 1 + 1) + 1)},\r", + " genres: [{ id: Math.floor(Math.random() * (6 - 1 + 1) + 1)}]\r", + " };\r", + " if (director!==null)\r", + " toReturn.directors = [{ id: director.id}];\r", + " return toReturn;\r", + " }\r", + "\r", + "\r", + " getWord(length = 1) {\r", + " let result = '';\r", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\r", + " const charactersLength = characters.length;\r", + " let counter = 0;\r", + " while (counter < length) {\r", + " result += characters.charAt(Math.floor(Math.random() * charactersLength));\r", + " counter += 1;\r", + " }\r", + " return result;\r", + " }\r", + "\r", + " getName(length = 1) {\r", + " let result = '';\r", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\r", + " const charactersLength = characters.length;\r", + " let counter = 0;\r", + " while (counter < length) {\r", + " result += characters.charAt(Math.floor(Math.random() * charactersLength));\r", + " counter += 1;\r", + " }\r", + " return result;\r", + " }\r", + "\r", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "http://localhost:8080" + }, + { + "key": "userName", + "value": "" + }, + { + "key": "userEmail", + "value": "" + }, + { + "key": "userId", + "value": "" + }, + { + "key": "item", + "value": "" + }, + { + "key": "itemName", + "value": "" + }, + { + "key": "itemAvailable", + "value": "" + }, + { + "key": "itemDescription", + "value": "" + }, + { + "key": "itemId", + "value": "" + }, + { + "key": "searchString", + "value": "" + }, + { + "key": "start", + "value": "" + }, + { + "key": "end", + "value": "" + }, + { + "key": "user1", + "value": "" + }, + { + "key": "user2", + "value": "" + }, + { + "key": "bookingId", + "value": "" + }, + { + "key": "booking", + "value": "" + }, + { + "key": "commentText", + "value": "" + } + ] +} diff --git a/src/main/java/ru/practicum/shareit/booking/Booking.java b/src/main/java/ru/practicum/shareit/booking/Booking.java index f14b217..ccfe012 100644 --- a/src/main/java/ru/practicum/shareit/booking/Booking.java +++ b/src/main/java/ru/practicum/shareit/booking/Booking.java @@ -9,9 +9,9 @@ @Getter @Setter +@ToString @RequiredArgsConstructor @AllArgsConstructor -@ToString @EqualsAndHashCode(of = {"id"}) @Entity @Table(name = "bookings") @@ -20,20 +20,22 @@ public class Booking { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(name = "start_", nullable = false) + @Column(name = "start_date", nullable = false) private LocalDateTime start; - @Column(name = "end_", nullable = false) + @Column(name = "end_date", nullable = false) private LocalDateTime end; @ManyToOne - @JoinColumn(name = "item", nullable = false) + @JoinColumn(name = "item_id", nullable = false) private Item item; @ManyToOne - @JoinColumn(name = "booker", nullable = false) + @JoinColumn(name = "booker_id", nullable = false) private User booker; @Column(length = 10) - private String status; + @Enumerated(EnumType.STRING) + private BookingStatus status; + } diff --git a/src/main/java/ru/practicum/shareit/booking/BookingController.java b/src/main/java/ru/practicum/shareit/booking/BookingController.java index b94493d..acaee0a 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -1,12 +1,88 @@ package ru.practicum.shareit.booking; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.*; + +import java.util.List; -/** - * TODO Sprint add-bookings. - */ @RestController @RequestMapping(path = "/bookings") public class BookingController { -} + + private final BookingService bookingService; + + public BookingController(BookingService bookingService) { + this.bookingService = bookingService; + } + + /** + * • Добавление нового запроса на бронирование. Запрос может быть создан любым пользователем, + * а затем подтверждён владельцем вещи. Эндпоинт — POST /bookings. + * После создания запрос находится в статусе WAITING — «ожидает подтверждения». + */ + @PostMapping + public BookingDtoOutput addBooking(@RequestHeader("X-Sharer-User-Id") Long bookerId, + @Valid @RequestBody BookingDtoInput bookingDto) { + return BookingMapper.toBookingDtoOutput(bookingService.addBooking(bookerId, bookingDto)); + } + + /** + * • Подтверждение или отклонение запроса на бронирование. Может быть выполнено только владельцем вещи. + * Затем статус бронирования становится либо APPROVED, либо REJECTED. + * Эндпоинт — PATCH /bookings/{bookingId}?approved={approved}, параметр approved может принимать + * значения true или false. + * + * @param ownerId ID владельца вещи. + * @param bookingId ID брони. + * @param approved True - подтверждено, False - отклонено. + * @return Обновленная бронь. + */ + @PatchMapping("/{bookingId}") + public BookingDtoOutput updateByOwner(@RequestHeader("X-Sharer-User-Id") Long ownerId, + @PathVariable Long bookingId, + @RequestParam Boolean approved) { + return BookingMapper.toBookingDtoOutput(bookingService.updateBooking(ownerId, bookingId, approved)); + } + + /** + * • Получение данных о конкретном бронировании (включая его статус). + * Может быть выполнено либо автором бронирования, либо владельцем вещи, + * к которой относится бронирование. + * Эндпоинт — GET /bookings/{bookingId}. + */ + @GetMapping("/{bookingId}") + public BookingDtoOutput getWithStatusById(@RequestHeader("X-Sharer-User-Id") Long userId, + @PathVariable Long bookingId) { + return BookingMapper.toBookingDtoOutput(bookingService.getWithStatusById(userId, bookingId)); + } + + /** + * • Получение списка всех бронирований текущего пользователя. + * Эндпоинт — GET /bookings?state={state}. + * Параметр state необязательный и по умолчанию равен ALL (англ. «все»). + * Также он может принимать значения CURRENT (англ. «текущие»), PAST (англ. «завершённые»), + * FUTURE (англ. «будущие»), WAITING (англ. «ожидающие подтверждения»), REJECTED (англ. «отклонённые»). + * Бронирования должны возвращаться отсортированными по дате от более новых к более старым. + */ + @GetMapping + public List getByUserId(@RequestHeader("X-Sharer-User-Id") Long userId, + @RequestParam(value = "state", + defaultValue = "ALL", required = false) String state) { + return bookingService.getByUserId(userId, state) + .stream().map(BookingMapper::toBookingDtoOutput).toList(); + } + + /** + * • Получение списка бронирований для всех вещей текущего пользователя. + * Эндпоинт — GET /bookings/owner?state={state}. + * Этот запрос имеет смысл для владельца хотя бы одной вещи. + * Работа параметра state аналогична его работе в предыдущем сценарии. + */ + @GetMapping("/owner") + public List getByOwnerId(@RequestHeader("X-Sharer-User-Id") Long userId, + @RequestParam(value = "state", defaultValue = "ALL", + required = false) String state) { + return bookingService.getByOwnerId(userId, state) + .stream().map(BookingMapper::toBookingDtoOutput).toList(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/BookingDto.java b/src/main/java/ru/practicum/shareit/booking/BookingDto.java index 5bce3d5..07632f8 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingDto.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingDto.java @@ -1,7 +1,28 @@ package ru.practicum.shareit.booking; -/** - * TODO Sprint add-bookings. - */ +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.shareit.item.Item; +import ru.practicum.shareit.user.User; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor public class BookingDto { + + private Long id; + + private LocalDateTime start; + + private LocalDateTime end; + + private Item item; + + private User booker; + + private BookingStatus status; + } diff --git a/src/main/java/ru/practicum/shareit/booking/BookingDtoInput.java b/src/main/java/ru/practicum/shareit/booking/BookingDtoInput.java new file mode 100644 index 0000000..b39e330 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingDtoInput.java @@ -0,0 +1,28 @@ +package ru.practicum.shareit.booking; + +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.shareit.validation.CreateObject; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class BookingDtoInput { + + @NotNull(groups = {CreateObject.class}, message = "При создании брони должна быть информация о вещи.") + private Long itemId; + + @FutureOrPresent(groups = {CreateObject.class}, message = "Дата не должна быть в прошлом") + @NotNull(groups = {CreateObject.class}, message = "Дата не должна быть пустой") + private LocalDateTime start; + + @FutureOrPresent(groups = {CreateObject.class}, message = "Дата не должна быть в прошлом") + @NotNull(groups = {CreateObject.class}, message = "Дата не должна быть пустой") + private LocalDateTime end; + +} diff --git a/src/main/java/ru/practicum/shareit/booking/BookingDtoOutput.java b/src/main/java/ru/practicum/shareit/booking/BookingDtoOutput.java new file mode 100644 index 0000000..c3b3476 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingDtoOutput.java @@ -0,0 +1,30 @@ +package ru.practicum.shareit.booking; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.shareit.item.ItemDtoShort; +import ru.practicum.shareit.user.UserDtoShort; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class BookingDtoOutput { + + private Long id; + + private LocalDateTime start; + + private LocalDateTime end; + + private ItemDtoShort item; + + private UserDtoShort booker; + + @JsonProperty("status") + private BookingStatus status; + +} diff --git a/src/main/java/ru/practicum/shareit/booking/BookingDtoShort.java b/src/main/java/ru/practicum/shareit/booking/BookingDtoShort.java new file mode 100644 index 0000000..de9f8b7 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingDtoShort.java @@ -0,0 +1,24 @@ +package ru.practicum.shareit.booking; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class BookingDtoShort { + + private Long id; + + private LocalDateTime start; + + private LocalDateTime end; + + @JsonProperty("status") + private BookingStatus status; + +} diff --git a/src/main/java/ru/practicum/shareit/booking/BookingMapper.java b/src/main/java/ru/practicum/shareit/booking/BookingMapper.java new file mode 100644 index 0000000..adf3f3d --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingMapper.java @@ -0,0 +1,65 @@ +package ru.practicum.shareit.booking; + +import ru.practicum.shareit.item.ItemMapper; +import ru.practicum.shareit.user.UserMapper; + +public class BookingMapper { + + public static BookingDtoOutput toBookingDtoOutput(Booking booking) { + if (booking == null) { + return null; + } + BookingDtoOutput bookingDto = new BookingDtoOutput(); + + bookingDto.setId(booking.getId()); + bookingDto.setStart(booking.getStart()); + bookingDto.setEnd(booking.getEnd()); + bookingDto.setItem(ItemMapper.toItemDtoShort(booking.getItem())); + bookingDto.setBooker(UserMapper.toUserDtoShort(booking.getBooker())); + bookingDto.setStatus(booking.getStatus()); + + return bookingDto; + } + + public static BookingDtoShort toBookingDtoShort(Booking booking) { + if (booking == null) { + return null; + } + BookingDtoShort bookingDto = new BookingDtoShort(); + + bookingDto.setId(booking.getId()); + bookingDto.setStart(booking.getStart()); + bookingDto.setEnd(booking.getEnd()); + bookingDto.setStatus(booking.getStatus()); + + return bookingDto; + } + + public static Booking toBooking(BookingDto bookingDto) { + Booking booking = new Booking(); + + if (bookingDto.getId() != null) { + if (bookingDto.getId() > 0) { + booking.setId(bookingDto.getId()); + } + } + if (bookingDto.getStart() != null) { + booking.setStart(bookingDto.getStart()); + } + if (bookingDto.getEnd() != null) { + booking.setEnd(bookingDto.getEnd()); + } + if (bookingDto.getItem() != null) { + booking.setItem(bookingDto.getItem()); + } + if (bookingDto.getBooker() != null) { + booking.setBooker(bookingDto.getBooker()); + } + if (bookingDto.getStatus() != null) { + booking.setStatus(bookingDto.getStatus()); + } + + return booking; + } + +} diff --git a/src/main/java/ru/practicum/shareit/booking/BookingRepository.java b/src/main/java/ru/practicum/shareit/booking/BookingRepository.java new file mode 100644 index 0000000..02638a2 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingRepository.java @@ -0,0 +1,32 @@ +package ru.practicum.shareit.booking; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.practicum.shareit.user.User; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public interface BookingRepository extends JpaRepository { + + List findAllByBookerOrderByStartDesc(User bookerFromDb); + + List findAllByBookerAndStartBeforeAndEndAfterOrderByStartDesc(User bookerFromDb, LocalDateTime nowDateTime, LocalDateTime nowDateTime1); + + List findAllByBookerAndEndIsBeforeOrderByStartDesc(User bookerFromDb, LocalDateTime nowDateTime); + + List findAllByBookerAndStartIsAfterOrderByStartDesc(User bookerFromDb, LocalDateTime nowDateTime); + + List findAllByBookerAndStatusEqualsOrderByStartDesc(User bookerFromDb, BookingStatus bookingStatus); + + List findAllByItem_OwnerOrderByStartDesc(User bookerFromDb); + + List findAllByItem_OwnerAndStartIsBeforeAndEndIsAfterOrderByStartDesc(User bookerFromDb, LocalDateTime nowDateTime, LocalDateTime nowDateTime1); + + List findAllByItem_OwnerAndEndIsBeforeOrderByStartDesc(User bookerFromDb, LocalDateTime nowDateTime); + + List findAllByItem_OwnerAndStartIsAfterOrderByStartDesc(User bookerFromDb, LocalDateTime nowDateTime); + + List findAllByItem_OwnerAndStatusEqualsOrderByStartDesc(User bookerFromDb, BookingStatus bookingStatus); +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/BookingService.java b/src/main/java/ru/practicum/shareit/booking/BookingService.java new file mode 100644 index 0000000..6c22e13 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingService.java @@ -0,0 +1,51 @@ +package ru.practicum.shareit.booking; + +import java.util.List; + +public interface BookingService { + /** + * Создание брони в БД. + * + * @param bookerId пользователь, пытающийся забронировать вещь. + * @param bookingDto создаваемая бронь. + * @return бронь из БД. + */ + Booking addBooking(Long bookerId, BookingDtoInput bookingDto); + + /** + * Обновить бронь в БД. + * + * @param ownerId хозяин вещи. + * @param bookingId ID брони. + * @param approved True - подтверждение со стороны хозяина вещи, False - наоборот. + * @return обновлённая бронь. + */ + Booking updateBooking(Long ownerId, Long bookingId, Boolean approved); + + /** + * • Получение данных о конкретном бронировании (включая его статус). + * Может быть выполнено либо автором бронирования, либо владельцем вещи, + * к которой относится бронирование. + */ + Booking getWithStatusById(Long userId, Long bookingId); + + /** + * • Получение списка всех бронирований текущего пользователя. + * Параметр state необязательный и по умолчанию равен ALL (англ. «все»). + * Также он может принимать значения CURRENT (англ. «текущие»), PAST (англ. «завершённые»), + * FUTURE (англ. «будущие»), WAITING (англ. «ожидающие подтверждения»), REJECTED (англ. «отклонённые»). + * Бронирования должны возвращаться отсортированными по дате от более новых к более старым. + */ + List getByUserId(Long userId, String state); + + /** + * • Получение списка бронирований для всех вещей текущего пользователя. + * + * @param userId ID хозяина вещей. + * @param state Параметр state необязательный и по умолчанию равен ALL (англ. «все»). + * Также он может принимать значения CURRENT (англ. «текущие»), PAST (англ. «завершённые»), + * FUTURE (англ. «будущие»), WAITING (англ. «ожидающие подтверждения»), REJECTED (англ. «отклонённые»). + * @return Бронирования должны возвращаться отсортированными по дате от более новых к более старым. + */ + List getByOwnerId(Long userId, String state); +} diff --git a/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java new file mode 100644 index 0000000..b60f0f4 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java @@ -0,0 +1,296 @@ +package ru.practicum.shareit.booking; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.exception.RestrictedAccessException; +import ru.practicum.shareit.exception.ValidationException; +import ru.practicum.shareit.item.Item; +import ru.practicum.shareit.item.ItemRepository; +import ru.practicum.shareit.user.User; +import ru.practicum.shareit.user.UserRepository; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +public class BookingServiceImpl implements BookingService { + private final BookingRepository bookingRepository; + private final ItemRepository itemRepository; + private final UserRepository userRepository; + + /** + * Создание брони в БД. + * + * @param bookerId пользователь, пытающийся забронировать вещь. + * @param inputBookingDto создаваемая бронь. + * @return бронь из БД. + */ + @Override + public Booking addBooking(Long bookerId, BookingDtoInput inputBookingDto) { + Item itemFromDB = itemRepository.findById(inputBookingDto.getItemId()) + .orElseThrow(() -> new NotFoundException("При создании бронирования не найдена " + + "вещь с ID = " + inputBookingDto.getItemId() + " в БД.")); + BookingDto bookingDto = new BookingDto(); + bookingDto.setStart(inputBookingDto.getStart()); + bookingDto.setEnd(inputBookingDto.getEnd()); + bookingDto.setItem(itemFromDB); + User bookerFromDb = userRepository.findById(bookerId) + .orElseThrow(() -> new NotFoundException("При " + + "создании бронирования не найден пользователь с ID = " + bookerId + " в БД.")); + validateBooking(bookingDto, itemFromDB, bookerFromDb); + if (!itemFromDB.getAvailable()) { + throw new ValidationException("Вещь нельзя забронировать, поскольку available = false."); + } + bookingDto.setStatus(BookingStatus.WAITING); + bookingDto.setItem(itemFromDB); + bookingDto.setBooker(bookerFromDb); + Booking result = bookingRepository.save(BookingMapper.toBooking(bookingDto)); + log.info("Создано бронирование с ID = '" + result.getId() + "'."); + return result; + } + + /** + * Обновить бронь в БД. + * + * @param ownerId хозяин вещи. + * @param bookingId ID брони. + * @param approved True - подтверждение со стороны хозяина вещи, + * False - отклонено хозяином вещи. + * @return обновлённая бронь. + */ + @Override + public Booking updateBooking(Long ownerId, Long bookingId, Boolean approved) { + Booking bookingFromBd = bookingRepository.findById(bookingId).orElseThrow(() -> new NotFoundException( + "При обновлении бронирования не найдено бронирование с ID = '" + bookingId + "' в БД.")); + if (bookingFromBd.getStatus().equals(BookingStatus.APPROVED) && approved) { + String message = "Данное бронирование уже было обработано и имеет статус '" + + bookingFromBd.getStatus() + "'."; + log.info(message); + throw new ValidationException(message); + } + User ownerFromDb = userRepository.findById(ownerId).orElseThrow(() -> new RestrictedAccessException("При " + + "обновлении бронирования не найден пользователь с ID = '" + ownerId + "' в БД.")); + List items = ownerFromDb.getItems(); + for (Item i : ownerFromDb.getItems()) { + if (i.getId().equals(bookingFromBd.getItem().getId())) { + bookingFromBd.setStatus(approved ? BookingStatus.APPROVED : BookingStatus.REJECTED); + Booking result = bookingRepository.save(bookingFromBd); + log.info("Бронирование с ID = '" + bookingId + "' обновлено."); + return result; + } + } + String message = "При обновлении брони у хозяина вещи эта вещь не найдена. Ошибка в запросе."; + log.info(message); + throw new NotFoundException(message); + } + + /** + * • Получение данных о конкретном бронировании (включая его статус). + * Может быть выполнено либо автором бронирования, либо владельцем вещи, + * к которой относится бронирование. + * + * @param userId ID пользователя, делающего запрос. + * @param bookingId ID брони. + */ + @Override + public Booking getWithStatusById(Long userId, Long bookingId) { + Booking booking = bookingRepository.findById(bookingId) + .orElseThrow(() -> new NotFoundException("Бронирование с ID = '" + bookingId + + "не найдено в БД при его получении.")); + if (userId.equals(booking.getBooker().getId()) || userId.equals(booking.getItem().getOwner().getId())) { + return booking; + } + throw new NotFoundException("Ошибка при получении брони с ID = '" + bookingId + + "'. Пользователь с ID = '" + userId + + "' не является ни хозяином, ни пользователем, забронировавшим вещь."); + } + + /** + * • Получение списка всех бронирований текущего пользователя. + * Параметр state необязательный и по умолчанию равен ALL (англ. «все»). + * Также он может принимать значения CURRENT (англ. «текущие»), PAST (англ. «завершённые»), + * FUTURE (англ. «будущие»), WAITING (англ. «ожидающие подтверждения»), REJECTED (англ. «отклонённые»). + * Бронирования должны возвращаться отсортированными по дате от более новых к более старым. + * + * @param userId ID пользователя. + * @param state статус бронирования. + */ + @Override + public List getByUserId(Long userId, String state) { + final LocalDateTime nowDateTime = LocalDateTime.now(); + BookingState bookingState; + + if (state.isBlank()) { + bookingState = BookingState.ALL; + } else { + try { + bookingState = BookingState.valueOf(state); + } catch (IllegalArgumentException ex) { + throw new ValidationException("Неизвестное состояние бронирования."); + } + } + User bookerFromDb = userRepository.findById(userId).orElseThrow(() -> new NotFoundException("При " + + "получении списка бронирований не найден пользователь (арендующий) с ID = " + userId + " в БД.")); + List result = new ArrayList<>(); + + switch (bookingState) { + case ALL: { + result = bookingRepository.findAllByBookerOrderByStartDesc(bookerFromDb); + break; + } + case CURRENT: { + result = bookingRepository.findAllByBookerAndStartBeforeAndEndAfterOrderByStartDesc( + bookerFromDb, nowDateTime, nowDateTime); + break; + } + case PAST: { + result = bookingRepository.findAllByBookerAndEndIsBeforeOrderByStartDesc( + bookerFromDb, nowDateTime); + break; + } + case FUTURE: { + result = bookingRepository.findAllByBookerAndStartIsAfterOrderByStartDesc( + bookerFromDb, nowDateTime); + break; + } + case WAITING: { + result = bookingRepository.findAllByBookerAndStatusEqualsOrderByStartDesc( + bookerFromDb, BookingStatus.WAITING); + break; + } + case REJECTED: { + result = bookingRepository.findAllByBookerAndStatusEqualsOrderByStartDesc( + bookerFromDb, BookingStatus.REJECTED); + break; + } + default: { + throw new ValidationException("Неизвестное состояние бронирования."); + } + } + + return result; + } + + /** + * • Получение списка бронирований для всех вещей текущего пользователя, то есть хозяина вещей. + * + * @param userId ID хозяина вещей. + * @param state Параметр state необязательный и по умолчанию равен ALL (англ. «все»). + * Также он может принимать значения CURRENT (англ. «текущие»), PAST (англ. «завершённые»), + * FUTURE (англ. «будущие»), WAITING (англ. «ожидающие подтверждения»), + * REJECTED (англ. «отклонённые»). + * @return Бронирования должны возвращаться отсортированными по дате от более новых к более старым. + */ + @Override + public List getByOwnerId(Long userId, String state) { + final LocalDateTime nowDateTime = LocalDateTime.now(); + BookingState bookingState; + + try { + bookingState = BookingState.valueOf(state); + } catch (IllegalArgumentException ex) { + throw new ValidationException("Неизвестное состояние бронирования."); + } + User bookerFromDb = userRepository.findById(userId).orElseThrow(() -> new NotFoundException("При " + + "получении списка бронирований не найден хозяин с ID = " + userId + " в БД.")); + List result = new ArrayList<>(); + + switch (bookingState) { + case ALL: { + result = bookingRepository.findAllByItem_OwnerOrderByStartDesc(bookerFromDb); + System.out.println(result); + break; + } + case CURRENT: { + result = bookingRepository.findAllByItem_OwnerAndStartIsBeforeAndEndIsAfterOrderByStartDesc( + bookerFromDb, nowDateTime, nowDateTime); + break; + } + case PAST: { + result = bookingRepository.findAllByItem_OwnerAndEndIsBeforeOrderByStartDesc( + bookerFromDb, nowDateTime); + break; + } + case FUTURE: { + result = bookingRepository.findAllByItem_OwnerAndStartIsAfterOrderByStartDesc( + bookerFromDb, nowDateTime); + break; + } + case WAITING: { + result = bookingRepository.findAllByItem_OwnerAndStatusEqualsOrderByStartDesc( + bookerFromDb, BookingStatus.WAITING); + break; + } + case REJECTED: { + result = bookingRepository.findAllByItem_OwnerAndStatusEqualsOrderByStartDesc( + bookerFromDb, BookingStatus.REJECTED); + break; + } + default: { + throw new ValidationException("Неизвестное состояние бронирования."); + } + } + + return result; + } + + /** + * Проверка при создании бронирования вещи. + * + * @param bookingDto бронь. + * @param item вещь. + * @param booker пользователь. + */ + private void validateBooking(BookingDto bookingDto, Item item, User booker) { + if (item.getOwner().equals(booker)) { + String message = "Создать бронь на свою вещь нельзя."; + log.info(message); + throw new ValidationException(message); + } + if (bookingDto.getStart() == null || bookingDto.getEnd() == null) { + String message = "Начало и окончание бронирования не может быть null."; + log.info(message); + throw new ValidationException(message); + } + if (bookingDto.getStart().equals(bookingDto.getEnd())) { + String message = "Начало и окончание бронирования не может быть одним и тем же временем."; + log.info(message); + throw new ValidationException(message); + } + + if (bookingDto.getStart().isBefore(LocalDateTime.now())) { + String message = "Начало бронирования не может быть в прошлом [" + bookingDto.getStart() + "]."; + log.info(message); + throw new ValidationException(message); + } + + if (bookingDto.getEnd().isBefore(LocalDateTime.now())) { + String message = "Окончание бронирования не может быть в прошлом [" + bookingDto.getEnd() + "]."; + log.info(message); + throw new ValidationException(message); + } + + if (bookingDto.getEnd().isBefore(bookingDto.getStart())) { + String message = "Окончание бронирования не может быть раньше его начала."; + log.info(message); + throw new ValidationException(message); + } + + List bookings = item.getBookings(); + if (!bookings.isEmpty()) { + for (Booking b : bookings) { + if (!(b.getEnd().isBefore(bookingDto.getStart()) || + b.getStart().isAfter(bookingDto.getStart()))) { + String message = "Найдено пересечение дат бронирования на эту вещь с name = " + item.getName() + "."; + log.debug(message); + throw new ValidationException(message); + } + } + } + } +} diff --git a/src/main/java/ru/practicum/shareit/booking/BookingState.java b/src/main/java/ru/practicum/shareit/booking/BookingState.java new file mode 100644 index 0000000..75be8b4 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingState.java @@ -0,0 +1,5 @@ +package ru.practicum.shareit.booking; + +public enum BookingState { + ALL, CURRENT, PAST, FUTURE, WAITING, REJECTED; +} diff --git a/src/main/java/ru/practicum/shareit/item/Comment.java b/src/main/java/ru/practicum/shareit/item/Comment.java new file mode 100644 index 0000000..107457e --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/Comment.java @@ -0,0 +1,36 @@ +package ru.practicum.shareit.item; + +import jakarta.persistence.*; +import lombok.*; +import ru.practicum.shareit.user.User; + +import java.time.LocalDateTime; + +@Getter +@Setter +@RequiredArgsConstructor +@AllArgsConstructor +@ToString +@EqualsAndHashCode(of = {"id"}) +@Entity +@Table(name = "comments") +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(length = 250) + private String text; + + @ManyToOne + @JoinColumn(name = "item_id", nullable = false) + private Item item; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + @Column(name = "created", nullable = false) + private LocalDateTime created; + +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/CommentDto.java b/src/main/java/ru/practicum/shareit/item/CommentDto.java new file mode 100644 index 0000000..bd2daa4 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/CommentDto.java @@ -0,0 +1,30 @@ +package ru.practicum.shareit.item; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.shareit.user.User; +import ru.practicum.shareit.validation.CreateObject; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CommentDto { + + private Long id; + + @NotNull(groups = {CreateObject.class}, message = "комментарий должен быть указан.") + @NotBlank(groups = {CreateObject.class}, message = "Комментарий не может быть пустым.") + private String text; + + private Item item; + + private User user; + + private LocalDateTime created; + +} diff --git a/src/main/java/ru/practicum/shareit/item/CommentDtoOutput.java b/src/main/java/ru/practicum/shareit/item/CommentDtoOutput.java new file mode 100644 index 0000000..2e76ce5 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/CommentDtoOutput.java @@ -0,0 +1,24 @@ +package ru.practicum.shareit.item; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CommentDtoOutput { + + private Long id; + + private String text; + + private ItemDtoShort item; + + private String authorName; + + private LocalDateTime created; + +} diff --git a/src/main/java/ru/practicum/shareit/item/CommentDtoShort.java b/src/main/java/ru/practicum/shareit/item/CommentDtoShort.java new file mode 100644 index 0000000..2e4fa12 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/CommentDtoShort.java @@ -0,0 +1,24 @@ +package ru.practicum.shareit.item; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CommentDtoShort { + + private Long id; + + private String text; + + private ItemDtoShort item; + + private String authorName; + + private LocalDateTime created; + +} diff --git a/src/main/java/ru/practicum/shareit/item/CommentMapper.java b/src/main/java/ru/practicum/shareit/item/CommentMapper.java new file mode 100644 index 0000000..60d09b9 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/CommentMapper.java @@ -0,0 +1,84 @@ +package ru.practicum.shareit.item; + +import java.time.LocalDateTime; + +public class CommentMapper { + + public static CommentDto toCommentDto(Comment comment) { + if (comment == null) { + return null; + } + CommentDto commentDto = new CommentDto(); + + commentDto.setId(comment.getId()); + commentDto.setText(comment.getText()); + commentDto.setItem(comment.getItem()); + commentDto.setUser(comment.getUser()); + commentDto.setCreated(comment.getCreated()); + + return commentDto; + } + + public static CommentDtoOutput toCommentDtoOutput(Comment comment) { + if (comment == null) { + return null; + } + CommentDtoOutput commentDto = new CommentDtoOutput(); + + commentDto.setId(comment.getId()); + commentDto.setText(comment.getText()); + commentDto.setItem(ItemMapper.toItemDtoShort(comment.getItem())); + commentDto.setAuthorName(comment.getUser().getName()); + commentDto.setCreated(comment.getCreated()); + + return commentDto; + } + + public static CommentDtoShort toCommentDtoShort(Comment comment) { + if (comment == null) { + return null; + } + CommentDtoShort commentDto = new CommentDtoShort(); + + commentDto.setId(comment.getId()); + commentDto.setText(comment.getText()); + commentDto.setItem(ItemMapper.toItemDtoShort(comment.getItem())); + commentDto.setAuthorName(comment.getUser().getName()); + commentDto.setCreated(comment.getCreated()); + + return commentDto; + } + + public static Comment toComment(CommentDto commentDto) { + Comment comment = new Comment(); + + if (commentDto.getId() != null) { + if (commentDto.getId() > 0) { + comment.setId(commentDto.getId()); + } + } + if (commentDto.getText() != null) { + if (!commentDto.getText().trim().isEmpty()) { + comment.setText(commentDto.getText().trim()); + } + } + if (commentDto.getItem().getId() != null) { + if (commentDto.getItem().getId() > 0) { + comment.setItem(commentDto.getItem()); + } + } + if (commentDto.getUser().getId() != null) { + if (commentDto.getUser().getId() > 0) { + comment.setUser(commentDto.getUser()); + } + } + if (commentDto.getCreated() != null) { + if (!commentDto.getCreated().isBefore(LocalDateTime.now())) { + comment.setCreated(commentDto.getCreated()); + } + } + + return comment; + } + +} diff --git a/src/main/java/ru/practicum/shareit/item/CommentRepository.java b/src/main/java/ru/practicum/shareit/item/CommentRepository.java new file mode 100644 index 0000000..55f2ae5 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/CommentRepository.java @@ -0,0 +1,9 @@ +package ru.practicum.shareit.item; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CommentRepository extends JpaRepository { + +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/Item.java b/src/main/java/ru/practicum/shareit/item/Item.java index 70da0eb..99e8100 100644 --- a/src/main/java/ru/practicum/shareit/item/Item.java +++ b/src/main/java/ru/practicum/shareit/item/Item.java @@ -1,16 +1,24 @@ package ru.practicum.shareit.item; +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; import jakarta.persistence.*; import lombok.*; -import ru.practicum.shareit.request.ItemRequest; +import ru.practicum.shareit.booking.Booking; +import ru.practicum.shareit.request.Request; import ru.practicum.shareit.user.User; +import java.util.List; + @Getter @Setter @RequiredArgsConstructor @AllArgsConstructor @ToString @EqualsAndHashCode(of = {"id"}) +@JsonIdentityInfo( + generator = ObjectIdGenerators.PropertyGenerator.class, + property = "id") @Entity @Table(name = "items") public class Item { @@ -24,14 +32,21 @@ public class Item { @Column(length = 250) private String description; - @Column(nullable = false) + @Column(name = "is_available", nullable = false) private Boolean available; @ManyToOne - @JoinColumn(name = "owner", nullable = false) + @JoinColumn(name = "owner_id", nullable = false) private User owner; @OneToOne - @JoinColumn(name = "request") - private ItemRequest request; + @JoinColumn(name = "request_id") + private Request request; + + @OneToMany(mappedBy = "item") + private List comments; + + @OneToMany(mappedBy = "item") + private List bookings; + } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/ItemController.java b/src/main/java/ru/practicum/shareit/item/ItemController.java index 349551e..593bce7 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemController.java +++ b/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -19,25 +19,28 @@ public ItemController(ItemService itemService) { @PostMapping @ResponseStatus(HttpStatus.CREATED) - public ItemDto addItem(@Validated(CreateObject.class) @RequestBody ItemDto itemDto, - @RequestHeader(value = "X-Sharer-User-Id") Long idUser) { - return ItemMapper.toItemDto(itemService.addItem(idUser, itemDto)); + public ItemDtoOutput addItem(@Validated(CreateObject.class) @RequestBody ItemDto itemDto, + @RequestHeader(value = "X-Sharer-User-Id") Long idUser) { + return ItemMapper.toItemDtoOutput(itemService.addItem(idUser, itemDto), idUser); } @PatchMapping("/{idItem}") - public ItemDto updateItem(@PathVariable Long idItem, @Validated(UpdateObject.class) @RequestBody ItemDto itemDto, - @RequestHeader(value = "X-Sharer-User-Id") Long idUser) { - return ItemMapper.toItemDto(itemService.updateItem(idUser, idItem, itemDto)); + public ItemDtoOutput updateItem(@PathVariable Long idItem, + @Validated(UpdateObject.class) @RequestBody ItemDto itemDto, + @RequestHeader(value = "X-Sharer-User-Id") Long idUser) { + return ItemMapper.toItemDtoOutput(itemService.updateItem(idUser, idItem, itemDto), idUser); } @GetMapping("/{idItem}") - public ItemDto getItemById(@PathVariable Long idItem) { - return ItemMapper.toItemDto(itemService.getItemById(idItem)); + public ItemDtoOutput getItemById(@PathVariable Long idItem, + @RequestHeader(value = "X-Sharer-User-Id") Long idUser) { + return ItemMapper.toItemDtoOutput(itemService.getItemById(idItem), idUser); } @GetMapping - public List getAllItemsByUser(@RequestHeader(value = "X-Sharer-User-Id") Long idUser) { - return itemService.getAllItems(idUser).stream().map(ItemMapper::toItemDto).toList(); + public List getAllItemsByUser(@RequestHeader(value = "X-Sharer-User-Id") Long idUser) { + return itemService.getAllItems(idUser).stream() + .map(item -> ItemMapper.toItemDtoOutput(item, idUser)).toList(); } @DeleteMapping("/{idItem}") @@ -47,8 +50,15 @@ public void removeItem(@PathVariable Long idItem, } @GetMapping("/search") - public List searchItemsByText(@RequestParam String text) { - return itemService.searchItems(text).stream().map(ItemMapper::toItemDto).toList(); + public List searchItemsByText(@RequestParam String text, + @RequestHeader(value = "X-Sharer-User-Id") Long idUser) { + return itemService.searchItems(text).stream() + .map(item -> ItemMapper.toItemDtoOutput(item, idUser)).toList(); } + @PostMapping("/{itemId}/comment") + public CommentDtoOutput addCommentToItem(@RequestHeader("X-Sharer-User-Id") Long userId, + @PathVariable Long itemId, @RequestBody CommentDto commentDto) { + return CommentMapper.toCommentDtoOutput((itemService.saveComment(userId, itemId, commentDto))); + } } diff --git a/src/main/java/ru/practicum/shareit/item/ItemDto.java b/src/main/java/ru/practicum/shareit/item/ItemDto.java index a024656..2fb9d3e 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemDto.java +++ b/src/main/java/ru/practicum/shareit/item/ItemDto.java @@ -1,15 +1,17 @@ package ru.practicum.shareit.item; -import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import ru.practicum.shareit.request.ItemRequest; +import ru.practicum.shareit.booking.Booking; +import ru.practicum.shareit.request.Request; import ru.practicum.shareit.user.User; import ru.practicum.shareit.validation.CreateObject; +import java.util.List; + @Data @AllArgsConstructor @NoArgsConstructor @@ -30,9 +32,14 @@ public class ItemDto { private User owner; - private ItemRequest request; + private Request request; + + private List comments; + private List bookings; + private Booking lastBooking; + private Booking nextBooking; } diff --git a/src/main/java/ru/practicum/shareit/item/ItemDtoOutput.java b/src/main/java/ru/practicum/shareit/item/ItemDtoOutput.java new file mode 100644 index 0000000..36ff82c --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/ItemDtoOutput.java @@ -0,0 +1,37 @@ +package ru.practicum.shareit.item; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.shareit.booking.BookingDtoShort; +import ru.practicum.shareit.request.Request; +import ru.practicum.shareit.user.UserDtoShort; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ItemDtoOutput { + + private Long id; + + private String name; + + private String description; + + private Boolean available; + + private UserDtoShort owner; + + private Request request; + + private List comments; + + private List bookings; + + private BookingDtoShort lastBooking; + + private BookingDtoShort nextBooking; + +} diff --git a/src/main/java/ru/practicum/shareit/item/ItemDtoShort.java b/src/main/java/ru/practicum/shareit/item/ItemDtoShort.java new file mode 100644 index 0000000..8dc3627 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/ItemDtoShort.java @@ -0,0 +1,20 @@ +package ru.practicum.shareit.item; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ItemDtoShort { + + private Long id; + + private String name; + + private String description; + + private Boolean available; + +} diff --git a/src/main/java/ru/practicum/shareit/item/ItemMapper.java b/src/main/java/ru/practicum/shareit/item/ItemMapper.java index b2c081b..d076fa1 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemMapper.java +++ b/src/main/java/ru/practicum/shareit/item/ItemMapper.java @@ -1,33 +1,159 @@ package ru.practicum.shareit.item; +import ru.practicum.shareit.booking.Booking; +import ru.practicum.shareit.booking.BookingMapper; +import ru.practicum.shareit.booking.BookingStatus; +import ru.practicum.shareit.user.UserMapper; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + public class ItemMapper { public static ItemDto toItemDto(Item item) { - return new ItemDto( - item.getId(), - item.getName(), - item.getDescription(), - item.getAvailable(), - item.getOwner(), - item.getRequest() - ); + if (item == null) { + return null; + } + ItemDto itemDto = new ItemDto(); + + itemDto.setId(item.getId()); + itemDto.setName(item.getName()); + itemDto.setDescription(item.getDescription()); + itemDto.setAvailable(item.getAvailable()); + itemDto.setOwner(item.getOwner()); + itemDto.setRequest(item.getRequest()); + itemDto.setComments(item.getComments()); + itemDto.setBookings(item.getBookings()); + + return itemDto; + } + + public static ItemDtoOutput toItemDtoOutput(Item item, Long userId) { + if (item == null) { + return null; + } + ItemDtoOutput itemDto = new ItemDtoOutput(); + + itemDto.setId(item.getId()); + itemDto.setName(item.getName()); + itemDto.setDescription(item.getDescription()); + itemDto.setAvailable(item.getAvailable()); + itemDto.setOwner(UserMapper.toUserDtoShort(item.getOwner())); + itemDto.setRequest(item.getRequest()); + List comments = Optional.ofNullable(item.getComments()).orElse(Collections.emptyList()); + if (!comments.isEmpty()) { + itemDto.setComments(comments.stream().map(CommentMapper::toCommentDtoShort).toList()); + } else { + itemDto.setComments(Collections.emptyList()); + } + List bookings = Optional.ofNullable(item.getBookings()).orElse(Collections.emptyList()); + if (!bookings.isEmpty()) { + if (itemDto.getOwner().getId().equals(userId)) { + itemDto.setBookings(bookings.stream().map(BookingMapper::toBookingDtoShort).toList()); + itemDto.setLastBooking(BookingMapper.toBookingDtoShort(findLastBooking(bookings))); + itemDto.setNextBooking(BookingMapper.toBookingDtoShort(findNextBooking(bookings))); + } else { + itemDto.setBookings(Collections.emptyList()); + itemDto.setLastBooking(null); + itemDto.setNextBooking(null); + } + } else { + itemDto.setBookings(Collections.emptyList()); + itemDto.setLastBooking(null); + itemDto.setNextBooking(null); + } + + return itemDto; + } + + public static ItemDtoShort toItemDtoShort(Item item) { + if (item == null) { + return null; + } + ItemDtoShort itemDto = new ItemDtoShort(); + + itemDto.setId(item.getId()); + itemDto.setName(item.getName()); + itemDto.setDescription(item.getDescription()); + itemDto.setAvailable(item.getAvailable()); + + return itemDto; } public static Item toItem(ItemDto itemDto) { + Item item = new Item(); + + if (itemDto.getId() != null) { + if (itemDto.getId() > 0) { + item.setId(itemDto.getId()); + } + } if (itemDto.getName() != null) { - itemDto.setName(itemDto.getName().trim()); + item.setName(itemDto.getName().trim()); } if (itemDto.getDescription() != null) { - itemDto.setDescription(itemDto.getDescription().trim()); - } - return new Item( - null, - itemDto.getName(), - itemDto.getDescription(), - itemDto.getAvailable(), - null, - null - ); + item.setDescription(itemDto.getDescription().trim()); + } + if (itemDto.getAvailable() != null) { + item.setAvailable(itemDto.getAvailable()); + } + if (itemDto.getOwner().getId() != null) { + if (itemDto.getOwner().getId() > 0) { + item.setOwner(itemDto.getOwner()); + } + } + if (itemDto.getRequest() != null) { + if (itemDto.getRequest().getId() > 0) { + item.setRequest(itemDto.getRequest()); + } + } + if (itemDto.getComments() != null) { + if (!itemDto.getComments().isEmpty()) { + item.setComments(itemDto.getComments()); + } + } + if (itemDto.getBookings() != null) { + if (!itemDto.getBookings().isEmpty()) { + item.setBookings(itemDto.getBookings()); + } + } + + return item; } + /** + * Метод поиска первой аренды после текущего момента времени. + * + * @param bookings список бронирований. + * @return следующее бронирование после текущего момента времени. + */ + private static Booking findNextBooking(List bookings) { + return Optional.ofNullable(bookings) + .filter(list -> !list.isEmpty()) + .flatMap(list -> list.stream() + .filter(b -> b.getStart().isAfter(LocalDateTime.now())) + .filter(b -> b.getStatus().equals(BookingStatus.APPROVED) || + b.getStatus().equals(BookingStatus.WAITING)) + .min(Comparator.comparing(Booking::getStart))) + .orElse(null); + } + + /** + * Метод поиска последней аренды до текущего момента времени. + * + * @param bookings список бронирований. + * @return последнее бронирование до текущего момента времени. + */ + private static Booking findLastBooking(List bookings) { + return Optional.ofNullable(bookings) + .filter(list -> !list.isEmpty()) + .flatMap(list -> list.stream() + .filter(b -> b.getEnd().isBefore(LocalDateTime.now())) + .filter(b -> b.getStatus().equals(BookingStatus.APPROVED)) + .max(Comparator.comparing(Booking::getEnd))) + .orElse(null); + } } diff --git a/src/main/java/ru/practicum/shareit/item/ItemService.java b/src/main/java/ru/practicum/shareit/item/ItemService.java index d89d720..ba5bf8a 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemService.java +++ b/src/main/java/ru/practicum/shareit/item/ItemService.java @@ -15,4 +15,7 @@ public interface ItemService { List getAllItems(Long idUser); List searchItems(String text); + + Comment saveComment(Long userId, Long itemId, CommentDto inputCommentDto); + } diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java index 8459a79..d47796f 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -3,12 +3,15 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; +import ru.practicum.shareit.booking.Booking; import ru.practicum.shareit.exception.NotFoundException; import ru.practicum.shareit.exception.RestrictedAccessException; import ru.practicum.shareit.exception.ValidationException; import ru.practicum.shareit.user.User; +import ru.practicum.shareit.user.UserMapper; import ru.practicum.shareit.user.UserRepository; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -18,10 +21,12 @@ public class ItemServiceImpl implements ItemService { private final ItemRepository itemRepository; private final UserRepository userRepository; + private final CommentRepository commentRepository; - public ItemServiceImpl(ItemRepository itemRepository, UserRepository userRepository) { + public ItemServiceImpl(ItemRepository itemRepository, UserRepository userRepository, CommentRepository commentRepository) { this.itemRepository = itemRepository; this.userRepository = userRepository; + this.commentRepository = commentRepository; } /** @@ -40,10 +45,10 @@ public Item addItem(Long idUser, ItemDto itemDto) { log.info(error); throw new NotFoundException(error); } + itemDto.setOwner(userRepository.findById(idUser).get()); Item result = ItemMapper.toItem(itemDto); - result.setOwner(userRepository.findById(idUser).get()); itemRepository.save(result); - log.info("Добавлена вещь [ {} ] пользователем [ {} ]", result, idUser); + log.info("Добавлена вещь [ {} ] пользователем [ {} ]", result.getId(), idUser); return result; } @@ -76,21 +81,21 @@ public Item updateItem(Long idUser, Long idItem, ItemDto itemDto) { log.info(error); throw new RestrictedAccessException(error); } - Item newItem = ItemMapper.toItem(itemDto); - newItem.setId(idItem); - newItem.setOwner(oldItem.get().getOwner()); - newItem.setRequest(oldItem.get().getRequest()); - if (newItem.getName() == null) { - newItem.setName(oldItem.get().getName()); + itemDto.setId(idItem); + itemDto.setOwner(oldItem.get().getOwner()); + itemDto.setRequest(oldItem.get().getRequest()); + if (itemDto.getName() == null) { + itemDto.setName(oldItem.get().getName()); } - if (newItem.getDescription() == null) { - newItem.setDescription(oldItem.get().getDescription()); + if (itemDto.getDescription() == null) { + itemDto.setDescription(oldItem.get().getDescription()); } - if (newItem.getAvailable() == null) { - newItem.setAvailable(oldItem.get().getAvailable()); + if (itemDto.getAvailable() == null) { + itemDto.setAvailable(oldItem.get().getAvailable()); } + Item newItem = ItemMapper.toItem(itemDto); itemRepository.save(newItem); - log.info("Обновлены данные вещи [ {} ]", newItem); + log.info("Обновлены данные вещи [ {} ]", ItemMapper.toItemDtoShort(newItem)); return newItem; } @@ -154,7 +159,7 @@ public List getAllItems(Long idUser) { throw new NotFoundException(error); } List result = itemRepository.findAllByOwner(user.get(), Sort.by("name")); - log.info("Получен список всех вещей пользователя [ {} ] : [ {} ]", user.get(), result); + log.info("Получен список всех вещей пользователя [ {} ] : [ {} ]", UserMapper.toUserDtoShort(user.get()), result.size()); return result; } @@ -170,7 +175,45 @@ public List searchItems(String text) { return List.of(); } List result = itemRepository.findAllByText(text); - log.info("Получен список вещей по поисковому запросу [ {} ] : [ {} ]", text, result); + log.info("Получен список вещей по поисковому запросу [ {} ] : [ {} ]", text, result.size()); + return result; + } + + /** + * Добавить комментарий к вещи пользователем, действительно бравшим вещь в аренду. + * + * @param bookerId ID пользователя, добавляющего комментарий. + * @param itemId ID вещи, которой оставляется комментарий. + */ + @Override + public Comment saveComment(Long bookerId, Long itemId, CommentDto commentDto) { + if (commentDto.getText().isBlank()) { + throw new ValidationException("Текст комментария не может быть пустым."); + } + User userFromBd = userRepository.findById(bookerId).orElseThrow(() -> + new NotFoundException("Ошибка при сохранении комментария к вещи с ID = " + itemId + + " пользователя с ID = " + bookerId + " в БД. В БД отсутствует запись о пользователе.")); + Item itemFromBd = itemRepository.findById(itemId).orElseThrow(() -> + new NotFoundException("Ошибка при сохранении комментария к вещи с ID = " + itemId + + " пользователя с ID = " + bookerId + " в БД. В БД отсутствует запись о вещи.")); + List bookings = itemFromBd.getBookings(); + boolean isBooker = false; + for (Booking b : bookings) { + if (b.getBooker().getId().equals(bookerId) && b.getEnd().isBefore(LocalDateTime.now())) { + isBooker = true; + break; + } + } + if (!isBooker) { + throw new ValidationException("Ошибка при сохранении комментария к вещи с ID = " + itemId + + " пользователя с ID = " + bookerId + " в БД. Пользователь не арендовал эту вещь."); + } + commentDto.setItem(itemFromBd); + commentDto.setUser(userFromBd); + commentDto.setCreated(LocalDateTime.now()); + Comment result = CommentMapper.toComment(commentDto); + result = commentRepository.save(result); + log.info("Комментарий к вещи успешно добавлен. Данные комментария: {}", CommentMapper.toCommentDtoShort(result)); return result; } diff --git a/src/main/java/ru/practicum/shareit/request/ItemRequest.java b/src/main/java/ru/practicum/shareit/request/Request.java similarity index 71% rename from src/main/java/ru/practicum/shareit/request/ItemRequest.java rename to src/main/java/ru/practicum/shareit/request/Request.java index e1d70a5..2e67520 100644 --- a/src/main/java/ru/practicum/shareit/request/ItemRequest.java +++ b/src/main/java/ru/practicum/shareit/request/Request.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import lombok.*; +import ru.practicum.shareit.item.Item; import ru.practicum.shareit.user.User; import java.time.LocalDateTime; @@ -13,8 +14,8 @@ @ToString @EqualsAndHashCode(of = {"id"}) @Entity -@Table(name = "item_requests") -public class ItemRequest { +@Table(name = "requests") +public class Request { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -23,9 +24,12 @@ public class ItemRequest { private String description; @ManyToOne - @JoinColumn(name = "requestor", nullable = false) + @JoinColumn(name = "requestor_id", nullable = false) private User requestor; + @OneToOne(mappedBy = "request") + private Item requestItem; + @Column(nullable = false) private LocalDateTime created; } diff --git a/src/main/java/ru/practicum/shareit/user/User.java b/src/main/java/ru/practicum/shareit/user/User.java index a1c7b66..07d790c 100644 --- a/src/main/java/ru/practicum/shareit/user/User.java +++ b/src/main/java/ru/practicum/shareit/user/User.java @@ -2,12 +2,17 @@ import jakarta.persistence.*; import lombok.*; +import ru.practicum.shareit.booking.Booking; +import ru.practicum.shareit.item.Comment; +import ru.practicum.shareit.item.Item; + +import java.util.List; @Getter @Setter +@ToString @RequiredArgsConstructor @AllArgsConstructor -@ToString @EqualsAndHashCode(of = {"id"}) @Entity @Table(name = "users") @@ -21,4 +26,14 @@ public class User { @Column(length = 50, unique = true) private String email; + + @OneToMany(mappedBy = "owner") + private List items; + + @OneToMany(mappedBy = "booker") + private List bookings; + + @OneToMany(mappedBy = "user") + private List comments; + } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/UserController.java b/src/main/java/ru/practicum/shareit/user/UserController.java index 2c9a481..85c05e8 100644 --- a/src/main/java/ru/practicum/shareit/user/UserController.java +++ b/src/main/java/ru/practicum/shareit/user/UserController.java @@ -19,23 +19,23 @@ public UserController(UserService userService) { @PostMapping @ResponseStatus(HttpStatus.CREATED) - public UserDto addUser(@Validated(CreateObject.class) @RequestBody UserDto userDto) { - return UserMapper.toUserDto(userService.addUser(userDto)); + public UserDtoOutput addUser(@Validated(CreateObject.class) @RequestBody UserDto userDto) { + return UserMapper.toUserDtoOutput(userService.addUser(userDto)); } @PatchMapping("/{idUser}") - public UserDto updateUser(@PathVariable Long idUser, @Validated(UpdateObject.class) @RequestBody UserDto userDto) { - return UserMapper.toUserDto(userService.updateUser(idUser, userDto)); + public UserDtoOutput updateUser(@PathVariable Long idUser, @Validated(UpdateObject.class) @RequestBody UserDto userDto) { + return UserMapper.toUserDtoOutput(userService.updateUser(idUser, userDto)); } @GetMapping("/{idUser}") - public UserDto getUserById(@PathVariable Long idUser) { - return UserMapper.toUserDto(userService.getUserById(idUser)); + public UserDtoOutput getUserById(@PathVariable Long idUser) { + return UserMapper.toUserDtoOutput(userService.getUserById(idUser)); } @GetMapping - public List getAllUsers() { - return userService.getAllUsers().stream().map(UserMapper::toUserDto).toList(); + public List getAllUsers() { + return userService.getAllUsers().stream().map(UserMapper::toUserDtoOutput).toList(); } @DeleteMapping("/{idUser}") diff --git a/src/main/java/ru/practicum/shareit/user/UserDto.java b/src/main/java/ru/practicum/shareit/user/UserDto.java index eb3315f..122a434 100644 --- a/src/main/java/ru/practicum/shareit/user/UserDto.java +++ b/src/main/java/ru/practicum/shareit/user/UserDto.java @@ -6,9 +6,14 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import ru.practicum.shareit.booking.Booking; +import ru.practicum.shareit.item.Comment; +import ru.practicum.shareit.item.Item; import ru.practicum.shareit.validation.CreateObject; import ru.practicum.shareit.validation.UpdateObject; +import java.util.List; + @Data @AllArgsConstructor @NoArgsConstructor @@ -24,4 +29,10 @@ public class UserDto { @Email(groups = {CreateObject.class, UpdateObject.class}, message = "Email должен быть указан корректно.") private String email; + private List items; + + private List bookings; + + private List comments; + } diff --git a/src/main/java/ru/practicum/shareit/user/UserDtoOutput.java b/src/main/java/ru/practicum/shareit/user/UserDtoOutput.java new file mode 100644 index 0000000..fd7a87b --- /dev/null +++ b/src/main/java/ru/practicum/shareit/user/UserDtoOutput.java @@ -0,0 +1,29 @@ +package ru.practicum.shareit.user; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.shareit.booking.BookingDtoShort; +import ru.practicum.shareit.item.CommentDtoShort; +import ru.practicum.shareit.item.ItemDtoShort; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class UserDtoOutput { + + private Long id; + + private String name; + + private String email; + + private List items; + + private List bookings; + + private List comments; + +} diff --git a/src/main/java/ru/practicum/shareit/user/UserDtoShort.java b/src/main/java/ru/practicum/shareit/user/UserDtoShort.java new file mode 100644 index 0000000..7bb1143 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/user/UserDtoShort.java @@ -0,0 +1,18 @@ +package ru.practicum.shareit.user; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class UserDtoShort { + + private Long id; + + private String name; + + private String email; + +} diff --git a/src/main/java/ru/practicum/shareit/user/UserMapper.java b/src/main/java/ru/practicum/shareit/user/UserMapper.java index 9e29e22..400b2ec 100644 --- a/src/main/java/ru/practicum/shareit/user/UserMapper.java +++ b/src/main/java/ru/practicum/shareit/user/UserMapper.java @@ -1,27 +1,96 @@ package ru.practicum.shareit.user; +import ru.practicum.shareit.booking.BookingMapper; +import ru.practicum.shareit.item.ItemMapper; +import ru.practicum.shareit.item.CommentMapper; + +import java.util.Collections; +import java.util.Optional; + public class UserMapper { public static UserDto toUserDto(User user) { - return new UserDto( - user.getId(), - user.getName(), - user.getEmail() - ); + if (user == null) { + return null; + } + UserDto userDto = new UserDto(); + + userDto.setId(user.getId()); + userDto.setName(user.getName()); + userDto.setEmail(user.getEmail()); + userDto.setItems(user.getItems()); + userDto.setBookings(user.getBookings()); + userDto.setComments(user.getComments()); + + return userDto; + } + + public static UserDtoOutput toUserDtoOutput(User user) { + if (user == null) { + return null; + } + UserDtoOutput userDto = new UserDtoOutput(); + + userDto.setId(user.getId()); + userDto.setName(user.getName()); + userDto.setEmail(user.getEmail()); + userDto.setItems(Optional.ofNullable(user.getItems()) + .map(items -> items.stream().map(ItemMapper::toItemDtoShort).toList()) + .orElse(Collections.emptyList())); + userDto.setBookings(Optional.ofNullable(user.getBookings()) + .map(bookings -> bookings.stream().map(BookingMapper::toBookingDtoShort).toList()) + .orElse(Collections.emptyList())); + userDto.setComments(Optional.ofNullable(user.getComments()) + .map(comments -> comments.stream().map(CommentMapper::toCommentDtoShort).toList()) + .orElse(Collections.emptyList())); + + return userDto; + } + + public static UserDtoShort toUserDtoShort(User user) { + if (user == null) { + return null; + } + UserDtoShort userDto = new UserDtoShort(); + + userDto.setId(user.getId()); + userDto.setName(user.getName()); + userDto.setEmail(user.getEmail()); + + return userDto; } public static User toUser(UserDto userDto) { - if (userDto.getEmail() != null) { - userDto.setEmail(userDto.getEmail().trim()); + User user = new User(); + + if (userDto.getId() != null) { + if (userDto.getId() > 0) { + user.setId(userDto.getId()); + } } if (userDto.getName() != null) { - userDto.setName(userDto.getName().trim()); + user.setName(userDto.getName().trim()); } - return new User( - null, - userDto.getName(), - userDto.getEmail() - ); + if (userDto.getEmail() != null) { + user.setEmail(userDto.getEmail().trim()); + } + if (userDto.getItems() != null) { + if (!userDto.getItems().isEmpty()) { + user.setItems(userDto.getItems()); + } + } + if (userDto.getBookings() != null) { + if (!userDto.getBookings().isEmpty()) { + user.setBookings(userDto.getBookings()); + } + } + if (userDto.getComments() != null) { + if (!userDto.getComments().isEmpty()) { + user.setComments(userDto.getComments()); + } + } + + return user; } } diff --git a/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java b/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java index a0bdc7e..0ca64a0 100644 --- a/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java @@ -58,21 +58,21 @@ public User updateUser(Long idUser, UserDto userDto) { log.error(error); throw new DataConflictException(error); } - User newUser = UserMapper.toUser(userDto); - newUser.setId(idUser); + userDto.setId(idUser); oldUser = userRepository.findById(idUser); if (oldUser.isPresent()) { - if (newUser.getName() == null) { - newUser.setName(oldUser.get().getName()); + if (userDto.getName() == null) { + userDto.setName(oldUser.get().getName()); } - if (newUser.getEmail() == null) { - newUser.setEmail(oldUser.get().getEmail()); + if (userDto.getEmail() == null) { + userDto.setEmail(oldUser.get().getEmail()); } } else { String error = "Пользователь с id [ " + idUser + " ] не найден в БД при обновлении данных пользователя."; log.info(error); throw new NotFoundException(error); } + User newUser = UserMapper.toUser(userDto); userRepository.save(newUser); log.info("Обновлен пользователь [ {} ]", newUser); return newUser; @@ -125,7 +125,8 @@ public void removeUser(Long idUser) { throw new NotFoundException(error); } userRepository.delete(oldUser.get()); - log.info("По id [ {} ] успешно удален пользователь [ {} ].", idUser, oldUser.get()); + log.info("По id [ {} ] успешно удален пользователь.", idUser); + // log.info("По id [ {} ] успешно удален пользователь [ {} ].", idUser, oldUser.get()); } } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 3dc5c53..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,16 +0,0 @@ -spring.jpa.hibernate.ddl-auto=none -spring.jpa.properties.hibernate.format_sql=true -spring.sql.init.mode=always - -logging.level.org.springframework.orm.jpa=INFO -logging.level.org.springframework.transaction=INFO -logging.level.org.springframework.transaction.interceptor=TRACE -logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG - -spring.datasource.url=jdbc:h2:file:./db/shareit -spring.datasource.driverClassName=org.h2.Driver -spring.datasource.username=sa -spring.datasource.password=password - -server.port=8080 - diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..faf6257 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,20 @@ +spring: + application.name: shareit + main.banner-mode: off + datasource: + url: jdbc:postgresql://localhost:5432/shareit + driver-class-name: org.postgresql.Driver + username: shareituser + password: 12345678 + jpa: + show-sql: true + hibernate: + ddl-auto: update + properties: + hibernate.jdbc.time_zone: UTC + dialect: org.hibernate.dialect.PostgreSQL95Dialect + format_sql: true + sql: + init: + mode: always + diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index b3a1d1e..0f68bd1 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,36 +1,44 @@ -DROP TABLE IF EXISTS bookings; -DROP TABLE IF EXISTS items; -DROP TABLE IF EXISTS item_requests; -DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS requests CASCADE; +DROP TABLE IF EXISTS comments CASCADE; +DROP TABLE IF EXISTS bookings CASCADE; +DROP TABLE IF EXISTS users CASCADE; +DROP TABLE IF EXISTS items CASCADE; -CREATE TABLE users ( - id LONG GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - name VARCHAR(100), - email VARCHAR(50) UNIQUE +CREATE TABLE IF NOT EXISTS users ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(100), + email VARCHAR(50) UNIQUE ); -CREATE TABLE item_requests ( - id LONG GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - description VARCHAR(250), - requestor LONG NOT NULL REFERENCES users (id) ON DELETE CASCADE ON UPDATE CASCADE, - created DATETIME DEFAULT (now()) +CREATE TABLE IF NOT EXISTS requests ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + description VARCHAR(250) NOT NULL, + requestor_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE ON UPDATE CASCADE, + created TIMESTAMP WITHOUT TIME ZONE DEFAULT (now()) ); -CREATE TABLE items ( - id LONG GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - name VARCHAR(100), - description VARCHAR(250), - available BOOL, - owner LONG NOT NULL REFERENCES users (id) ON DELETE CASCADE ON UPDATE CASCADE, - request LONG DEFAULT NULL REFERENCES item_requests (id) ON DELETE SET NULL ON UPDATE CASCADE +CREATE TABLE IF NOT EXISTS items ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(100), + description VARCHAR(250), + is_available BOOL DEFAULT FALSE, + owner_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE ON UPDATE CASCADE, + request_id BIGINT DEFAULT NULL REFERENCES requests (id) ON DELETE SET NULL ON UPDATE CASCADE ); -CREATE TABLE bookings ( - id LONG GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - start_ DATETIME, - end_ DATETIME, - item LONG NOT NULL REFERENCES items (id) ON DELETE CASCADE ON UPDATE CASCADE, - booker LONG NOT NULL REFERENCES users (id) ON DELETE CASCADE ON UPDATE CASCADE, - status VARCHAR(10) DEFAULT 'WAITING' +CREATE TABLE IF NOT EXISTS bookings ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + start_date TIMESTAMP WITHOUT TIME ZONE, + end_date TIMESTAMP WITHOUT TIME ZONE, + item_id BIGINT NOT NULL REFERENCES items (id) ON DELETE CASCADE ON UPDATE CASCADE, + booker_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE ON UPDATE CASCADE, + status VARCHAR(10) DEFAULT 'WAITING' ); +CREATE TABLE IF NOT EXISTS comments ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + text VARCHAR(250), + item_id BIGINT NOT NULL REFERENCES items (id) ON DELETE CASCADE ON UPDATE CASCADE, + user_id BIGINT DEFAULT NULL REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE, + created TIMESTAMP WITHOUT TIME ZONE DEFAULT (now()) +); From 7e75c27c1c178d697d754954c2d88db1a3da2f57 Mon Sep 17 00:00:00 2001 From: Sergey Filippovskikh <116564864+SergikF@users.noreply.github.com> Date: Thu, 17 Apr 2025 03:17:34 +0300 Subject: [PATCH 2/4] =?UTF-8?q?=D0=92=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=2015=20=D1=81=D0=BF=D1=80=D0=B8=D0=BD=D1=82?= =?UTF-8?q?=D0=B0.=20=D0=9F=D0=B5=D1=80=D0=B2=D0=B8=D1=87=D0=BD=D0=B0?= =?UTF-8?q?=D1=8F=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B0=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ru/practicum/shareit/ShareItTests.java | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 src/test/java/ru/practicum/shareit/ShareItTests.java diff --git a/src/test/java/ru/practicum/shareit/ShareItTests.java b/src/test/java/ru/practicum/shareit/ShareItTests.java deleted file mode 100644 index 4d79052..0000000 --- a/src/test/java/ru/practicum/shareit/ShareItTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package ru.practicum.shareit; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ShareItTests { - - @Test - void contextLoads() { - } - -} From cbd0d413a57d719943d5d684d7e56f20c17e128e Mon Sep 17 00:00:00 2001 From: Sergey Filippovskikh <116564864+SergikF@users.noreply.github.com> Date: Thu, 17 Apr 2025 10:54:43 +0300 Subject: [PATCH 3/4] =?UTF-8?q?=D0=92=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=2015=20=D1=81=D0=BF=D1=80=D0=B8=D0=BD=D1=82?= =?UTF-8?q?=D0=B0.=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=87=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shareit/booking/BookingController.java | 6 +-- .../shareit/booking/BookingService.java | 37 +------------------ .../shareit/booking/BookingServiceImpl.java | 24 ++---------- .../shareit/item/ItemController.java | 6 +-- .../shareit/item/ItemServiceImpl.java | 4 +- .../shareit/user/UserController.java | 6 +-- 6 files changed, 12 insertions(+), 71 deletions(-) diff --git a/src/main/java/ru/practicum/shareit/booking/BookingController.java b/src/main/java/ru/practicum/shareit/booking/BookingController.java index acaee0a..40df0bc 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -1,20 +1,18 @@ package ru.practicum.shareit.booking; import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping(path = "/bookings") +@RequiredArgsConstructor public class BookingController { private final BookingService bookingService; - public BookingController(BookingService bookingService) { - this.bookingService = bookingService; - } - /** * • Добавление нового запроса на бронирование. Запрос может быть создан любым пользователем, * а затем подтверждён владельцем вещи. Эндпоинт — POST /bookings. diff --git a/src/main/java/ru/practicum/shareit/booking/BookingService.java b/src/main/java/ru/practicum/shareit/booking/BookingService.java index 6c22e13..9a731d5 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingService.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingService.java @@ -3,49 +3,14 @@ import java.util.List; public interface BookingService { - /** - * Создание брони в БД. - * - * @param bookerId пользователь, пытающийся забронировать вещь. - * @param bookingDto создаваемая бронь. - * @return бронь из БД. - */ + Booking addBooking(Long bookerId, BookingDtoInput bookingDto); - /** - * Обновить бронь в БД. - * - * @param ownerId хозяин вещи. - * @param bookingId ID брони. - * @param approved True - подтверждение со стороны хозяина вещи, False - наоборот. - * @return обновлённая бронь. - */ Booking updateBooking(Long ownerId, Long bookingId, Boolean approved); - /** - * • Получение данных о конкретном бронировании (включая его статус). - * Может быть выполнено либо автором бронирования, либо владельцем вещи, - * к которой относится бронирование. - */ Booking getWithStatusById(Long userId, Long bookingId); - /** - * • Получение списка всех бронирований текущего пользователя. - * Параметр state необязательный и по умолчанию равен ALL (англ. «все»). - * Также он может принимать значения CURRENT (англ. «текущие»), PAST (англ. «завершённые»), - * FUTURE (англ. «будущие»), WAITING (англ. «ожидающие подтверждения»), REJECTED (англ. «отклонённые»). - * Бронирования должны возвращаться отсортированными по дате от более новых к более старым. - */ List getByUserId(Long userId, String state); - /** - * • Получение списка бронирований для всех вещей текущего пользователя. - * - * @param userId ID хозяина вещей. - * @param state Параметр state необязательный и по умолчанию равен ALL (англ. «все»). - * Также он может принимать значения CURRENT (англ. «текущие»), PAST (англ. «завершённые»), - * FUTURE (англ. «будущие»), WAITING (англ. «ожидающие подтверждения»), REJECTED (англ. «отклонённые»). - * @return Бронирования должны возвращаться отсортированными по дате от более новых к более старым. - */ List getByOwnerId(Long userId, String state); } diff --git a/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java index b60f0f4..68ffa41 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java @@ -50,7 +50,7 @@ public Booking addBooking(Long bookerId, BookingDtoInput inputBookingDto) { bookingDto.setItem(itemFromDB); bookingDto.setBooker(bookerFromDb); Booking result = bookingRepository.save(BookingMapper.toBooking(bookingDto)); - log.info("Создано бронирование с ID = '" + result.getId() + "'."); + log.info("Создано бронирование с ID = [ {} ].", result.getId()); return result; } @@ -80,7 +80,7 @@ public Booking updateBooking(Long ownerId, Long bookingId, Boolean approved) { if (i.getId().equals(bookingFromBd.getItem().getId())) { bookingFromBd.setStatus(approved ? BookingStatus.APPROVED : BookingStatus.REJECTED); Booking result = bookingRepository.save(bookingFromBd); - log.info("Бронирование с ID = '" + bookingId + "' обновлено."); + log.info("Бронирование с ID = [ {} ] обновлено.", bookingId); return result; } } @@ -252,29 +252,13 @@ private void validateBooking(BookingDto bookingDto, Item item, User booker) { log.info(message); throw new ValidationException(message); } - if (bookingDto.getStart() == null || bookingDto.getEnd() == null) { - String message = "Начало и окончание бронирования не может быть null."; - log.info(message); - throw new ValidationException(message); - } + if (bookingDto.getStart().equals(bookingDto.getEnd())) { String message = "Начало и окончание бронирования не может быть одним и тем же временем."; log.info(message); throw new ValidationException(message); } - if (bookingDto.getStart().isBefore(LocalDateTime.now())) { - String message = "Начало бронирования не может быть в прошлом [" + bookingDto.getStart() + "]."; - log.info(message); - throw new ValidationException(message); - } - - if (bookingDto.getEnd().isBefore(LocalDateTime.now())) { - String message = "Окончание бронирования не может быть в прошлом [" + bookingDto.getEnd() + "]."; - log.info(message); - throw new ValidationException(message); - } - if (bookingDto.getEnd().isBefore(bookingDto.getStart())) { String message = "Окончание бронирования не может быть раньше его начала."; log.info(message); @@ -286,7 +270,7 @@ private void validateBooking(BookingDto bookingDto, Item item, User booker) { for (Booking b : bookings) { if (!(b.getEnd().isBefore(bookingDto.getStart()) || b.getStart().isAfter(bookingDto.getStart()))) { - String message = "Найдено пересечение дат бронирования на эту вещь с name = " + item.getName() + "."; + String message = "Найдено пересечение дат бронирования на вещь с name = " + item.getName() + "."; log.debug(message); throw new ValidationException(message); } diff --git a/src/main/java/ru/practicum/shareit/item/ItemController.java b/src/main/java/ru/practicum/shareit/item/ItemController.java index 593bce7..affabdd 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemController.java +++ b/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -1,5 +1,6 @@ package ru.practicum.shareit.item; +import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -10,13 +11,10 @@ @RestController @RequestMapping("/items") +@RequiredArgsConstructor public class ItemController { private final ItemService itemService; - public ItemController(ItemService itemService) { - this.itemService = itemService; - } - @PostMapping @ResponseStatus(HttpStatus.CREATED) public ItemDtoOutput addItem(@Validated(CreateObject.class) @RequestBody ItemDto itemDto, diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java index d47796f..edaf54d 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -187,9 +187,7 @@ public List searchItems(String text) { */ @Override public Comment saveComment(Long bookerId, Long itemId, CommentDto commentDto) { - if (commentDto.getText().isBlank()) { - throw new ValidationException("Текст комментария не может быть пустым."); - } + User userFromBd = userRepository.findById(bookerId).orElseThrow(() -> new NotFoundException("Ошибка при сохранении комментария к вещи с ID = " + itemId + " пользователя с ID = " + bookerId + " в БД. В БД отсутствует запись о пользователе.")); diff --git a/src/main/java/ru/practicum/shareit/user/UserController.java b/src/main/java/ru/practicum/shareit/user/UserController.java index 85c05e8..38dff38 100644 --- a/src/main/java/ru/practicum/shareit/user/UserController.java +++ b/src/main/java/ru/practicum/shareit/user/UserController.java @@ -1,5 +1,6 @@ package ru.practicum.shareit.user; +import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -10,13 +11,10 @@ @RestController @RequestMapping("/users") +@RequiredArgsConstructor public class UserController { private final UserService userService; - public UserController(UserService userService) { - this.userService = userService; - } - @PostMapping @ResponseStatus(HttpStatus.CREATED) public UserDtoOutput addUser(@Validated(CreateObject.class) @RequestBody UserDto userDto) { From 55457b6ec96a4dfa09c2c83850db89f57bfa3c4d Mon Sep 17 00:00:00 2001 From: Sergey Filippovskikh <116564864+SergikF@users.noreply.github.com> Date: Thu, 17 Apr 2025 11:10:40 +0300 Subject: [PATCH 4/4] =?UTF-8?q?=D0=92=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=2015=20=D1=81=D0=BF=D1=80=D0=B8=D0=BD=D1=82?= =?UTF-8?q?=D0=B0.=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=87=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ru/practicum/shareit/booking/BookingRepository.java | 4 ++-- .../ru/practicum/shareit/booking/BookingServiceImpl.java | 8 ++++---- .../java/ru/practicum/shareit/booking/BookingStatus.java | 3 +-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/ru/practicum/shareit/booking/BookingRepository.java b/src/main/java/ru/practicum/shareit/booking/BookingRepository.java index 02638a2..72ad8ac 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingRepository.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingRepository.java @@ -18,7 +18,7 @@ public interface BookingRepository extends JpaRepository { List findAllByBookerAndStartIsAfterOrderByStartDesc(User bookerFromDb, LocalDateTime nowDateTime); - List findAllByBookerAndStatusEqualsOrderByStartDesc(User bookerFromDb, BookingStatus bookingStatus); + List findAllByBookerAndStatusEqualsOrderByStartDesc(User bookerFromDb, BookingState bookingState); List findAllByItem_OwnerOrderByStartDesc(User bookerFromDb); @@ -28,5 +28,5 @@ public interface BookingRepository extends JpaRepository { List findAllByItem_OwnerAndStartIsAfterOrderByStartDesc(User bookerFromDb, LocalDateTime nowDateTime); - List findAllByItem_OwnerAndStatusEqualsOrderByStartDesc(User bookerFromDb, BookingStatus bookingStatus); + List findAllByItem_OwnerAndStatusEqualsOrderByStartDesc(User bookerFromDb, BookingState bookingState); } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java index 68ffa41..991eba2 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java @@ -160,12 +160,12 @@ public List getByUserId(Long userId, String state) { } case WAITING: { result = bookingRepository.findAllByBookerAndStatusEqualsOrderByStartDesc( - bookerFromDb, BookingStatus.WAITING); + bookerFromDb, BookingState.WAITING); break; } case REJECTED: { result = bookingRepository.findAllByBookerAndStatusEqualsOrderByStartDesc( - bookerFromDb, BookingStatus.REJECTED); + bookerFromDb, BookingState.REJECTED); break; } default: { @@ -223,12 +223,12 @@ public List getByOwnerId(Long userId, String state) { } case WAITING: { result = bookingRepository.findAllByItem_OwnerAndStatusEqualsOrderByStartDesc( - bookerFromDb, BookingStatus.WAITING); + bookerFromDb, BookingState.WAITING); break; } case REJECTED: { result = bookingRepository.findAllByItem_OwnerAndStatusEqualsOrderByStartDesc( - bookerFromDb, BookingStatus.REJECTED); + bookerFromDb, BookingState.REJECTED); break; } default: { diff --git a/src/main/java/ru/practicum/shareit/booking/BookingStatus.java b/src/main/java/ru/practicum/shareit/booking/BookingStatus.java index 51269e0..22928de 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingStatus.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingStatus.java @@ -3,6 +3,5 @@ public enum BookingStatus { WAITING, APPROVED, - REJECTED, - CANCELED + REJECTED }