From 27ee5aa50c8747eaa671d9e72a0ad26107d1de5f Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Thu, 22 May 2025 22:50:35 +0700 Subject: [PATCH 01/29] =?UTF-8?q?=D1=81=D1=85=D0=B5=D0=BC=D0=B0=20=D0=B1?= =?UTF-8?q?=D0=B0=D0=B7=D1=8B=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 6 +- ewm-service/README.md | 7 ++- ewm-service/pom.xml | 12 ++++ ewm-service/schema.png | Bin 0 -> 55245 bytes .../src/main/resources/application.properties | 13 ++++ ewm-service/src/main/resources/schema.sql | 56 ++++++++++++++++++ 6 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 ewm-service/schema.png create mode 100644 ewm-service/src/main/resources/schema.sql diff --git a/docker-compose.yml b/docker-compose.yml index a02f49e..353a335 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,9 +45,9 @@ services: ports: - "5434:5434" environment: - - POSTGRES_PASSWORD=statewm - - POSTGRES_USER=statewm - - POSTGRES_DB=statewm + - POSTGRES_PASSWORD=ewmdb + - POSTGRES_USER=ewmdb + - POSTGRES_DB=ewmdb healthcheck: test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER timeout: 5s diff --git a/ewm-service/README.md b/ewm-service/README.md index f6eea04..0eb2ec3 100644 --- a/ewm-service/README.md +++ b/ewm-service/README.md @@ -1 +1,6 @@ -# ewm-service \ No newline at end of file +# ewm-service +Сервис событий + +Схема базы данных. +![схема базы данных](/schema.png) + diff --git a/ewm-service/pom.xml b/ewm-service/pom.xml index 9eb9b4f..36afbf5 100644 --- a/ewm-service/pom.xml +++ b/ewm-service/pom.xml @@ -51,6 +51,18 @@ compile + + org.postgresql + postgresql + runtime + + + + com.h2database + h2 + runtime + + ru.practicum stat-client diff --git a/ewm-service/schema.png b/ewm-service/schema.png new file mode 100644 index 0000000000000000000000000000000000000000..269b05a01c0bdec93399ee578647387104231c02 GIT binary patch literal 55245 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 Date: Sun, 25 May 2025 22:32:36 +0700 Subject: [PATCH 02/29] feat: /admin/users --- docker-compose.yml | 1 + ewm-service/pom.xml | 5 + .../evmsevice/client/StatsClient.java | 11 +++ .../controller/AdminUserController.java | 71 ++++++++++++++ .../ru/practicum/evmsevice/dto/ApiError.java | 20 ++++ .../ru/practicum/evmsevice/dto/UserDto.java | 21 +++++ .../practicum/evmsevice/dto/UserShortDto.java | 17 ++++ .../exception/InternalServerException.java | 7 ++ .../exception/NotFoundException.java | 7 ++ .../evmsevice/mapper/UserMapper.java | 32 +++++++ .../ru/practicum/evmsevice/model/User.java | 26 ++++++ .../evmsevice/repository/UserRepository.java | 7 ++ .../evmsevice/service/AdminUserService.java | 15 +++ .../service/AdminUserServiceImpl.java | 44 +++++++++ .../src/main/resources/application.properties | 3 +- ewm-service/src/main/resources/schema.sql | 92 ++++++++++--------- 16 files changed, 333 insertions(+), 46 deletions(-) create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminUserController.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/dto/ApiError.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/dto/UserDto.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/dto/UserShortDto.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/exception/InternalServerException.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/exception/NotFoundException.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/mapper/UserMapper.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/model/User.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/repository/UserRepository.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminUserService.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminUserServiceImpl.java diff --git a/docker-compose.yml b/docker-compose.yml index 353a335..6a9811d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,6 +35,7 @@ services: ports: - "8080:8080" depends_on: + - ewm-db - stats-server environment: - STATSERVER_URL=http://stats-server:9090 diff --git a/ewm-service/pom.xml b/ewm-service/pom.xml index 36afbf5..d029e3e 100644 --- a/ewm-service/pom.xml +++ b/ewm-service/pom.xml @@ -63,6 +63,11 @@ runtime + + org.springframework.boot + spring-boot-starter-data-jpa + + ru.practicum stat-client diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java b/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java index 811b2a1..c368f0c 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java @@ -1,5 +1,6 @@ package ru.practicum.evmsevice.client; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.client.RestTemplateBuilder; @@ -11,6 +12,7 @@ import ru.practicum.statclient.BaseClient; import ru.practicum.statdto.HitDto; +import java.time.LocalDateTime; import java.util.Map; @Component @@ -35,4 +37,13 @@ public void post(HitDto dto) { public ResponseEntity get(Map parameters) { return makeAndSendRequest(HttpMethod.GET, PREFIX_STATS, parameters, null); } + + public void hitInfo(String appName, HttpServletRequest request) { + HitDto hitDto = new HitDto(); + hitDto.setApp(appName); + hitDto.setUri(request.getRequestURI()); + hitDto.setIp(request.getRemoteAddr()); + hitDto.setTimestamp(LocalDateTime.now()); + post(hitDto); + } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminUserController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminUserController.java new file mode 100644 index 0000000..f4bb0fb --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminUserController.java @@ -0,0 +1,71 @@ +package ru.practicum.evmsevice.controller; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.evmsevice.client.StatsClient; +import ru.practicum.evmsevice.dto.UserDto; +import ru.practicum.evmsevice.mapper.UserMapper; +import ru.practicum.evmsevice.model.User; +import ru.practicum.evmsevice.repository.UserRepository; +import ru.practicum.evmsevice.service.AdminUserService; +import ru.practicum.statdto.HitDto; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +/** + * Класс административных запросов по объектам "User" + */ +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping("/admin/users") +public class AdminUserController { + @Value("${spring.application.name}") + private String appName; + private static final DateTimeFormatter DATA_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private final StatsClient statsClient; + private final AdminUserService adminUserService; + + @GetMapping + @ResponseStatus(HttpStatus.OK) + public List getUsers(HttpServletRequest request) { + log.info("{} запрашивает список пользователей.", request.getRemoteUser()); + statsClient.hitInfo(appName, request); + return adminUserService.getUsers().stream() + .map(UserMapper::toUserDto) + .toList(); + } + + @GetMapping("/{id}") + @ResponseStatus(HttpStatus.OK) + public UserDto getUser(HttpServletRequest request, @PathVariable Integer id) { + log.info("Выполняем поиск пользователя id={}.", id); + statsClient.hitInfo(appName, request); + User user = adminUserService.getUserById(id); + return UserMapper.toUserDto(user); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public UserDto createUser(@Validated @RequestBody UserDto userDto, HttpServletRequest request) { + log.info("Создаем нового пользователя {}", userDto.toString()); + statsClient.hitInfo(appName, request); + User savedUser =adminUserService.addUser(UserMapper.toUser(userDto)); + return UserMapper.toUserDto(savedUser); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.OK) + public void deleteUser(HttpServletRequest request, @PathVariable Integer id) { + log.info("Удаляем пользователя {}", id); + statsClient.hitInfo(appName, request); + adminUserService.deleteUser(id); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/ApiError.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/ApiError.java new file mode 100644 index 0000000..1def575 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/ApiError.java @@ -0,0 +1,20 @@ +package ru.practicum.evmsevice.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Setter +@Getter +@NoArgsConstructor +public class ApiError { + private String status; + private String reason; + private String message; + private LocalDateTime timestamp; + private List errors = new ArrayList<>(); +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UserDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UserDto.java new file mode 100644 index 0000000..06d509e --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UserDto.java @@ -0,0 +1,21 @@ +package ru.practicum.evmsevice.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class UserDto { + private Integer id; + @NotBlank(message = "Имя не может быть пустым") + private String name; + @NotBlank(message = "Email не может быть пустым") + @Email(message = "Email должен удовлетворять правилам формирования почтовых адресов.") + private String email; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UserShortDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UserShortDto.java new file mode 100644 index 0000000..1bc655f --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UserShortDto.java @@ -0,0 +1,17 @@ +package ru.practicum.evmsevice.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class UserShortDto { + private Integer id; + @NotBlank(message = "Имя не может быть пустым") + private String name; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/exception/InternalServerException.java b/ewm-service/src/main/java/ru/practicum/evmsevice/exception/InternalServerException.java new file mode 100644 index 0000000..8683da7 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/exception/InternalServerException.java @@ -0,0 +1,7 @@ +package ru.practicum.evmsevice.exception; + +public class InternalServerException extends RuntimeException { + public InternalServerException(String message) { + super(message); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/exception/NotFoundException.java b/ewm-service/src/main/java/ru/practicum/evmsevice/exception/NotFoundException.java new file mode 100644 index 0000000..764150a --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package ru.practicum.evmsevice.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/UserMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/UserMapper.java new file mode 100644 index 0000000..fda352f --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/UserMapper.java @@ -0,0 +1,32 @@ +package ru.practicum.evmsevice.mapper; + +import ru.practicum.evmsevice.dto.UserDto; +import ru.practicum.evmsevice.dto.UserShortDto; +import ru.practicum.evmsevice.model.User; + +public class UserMapper { + private UserMapper() {} + public static User toUser(UserDto dto) { + User user = new User(); + if (dto.getId() != null) { + user.setId(dto.getId()); + } + user.setName(dto.getName()); + user.setEmail(dto.getEmail()); + return user; + } + public static UserDto toUserDto(User user) { + UserDto dto = new UserDto(); + dto.setId(user.getId()); + dto.setName(user.getName()); + dto.setEmail(user.getEmail()); + return dto; + } + + public static UserShortDto toUserShortDto(User user) { + UserShortDto dto = new UserShortDto(); + dto.setId(user.getId()); + dto.setName(user.getName()); + return dto; + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/User.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/User.java new file mode 100644 index 0000000..390b7a7 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/User.java @@ -0,0 +1,26 @@ +package ru.practicum.evmsevice.model; + +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * Класс описания пользователя + */ +@Entity +@Setter +@Getter +@Table(name = "users", schema = "public") +@EqualsAndHashCode(of = {"name", "email"}) +@NoArgsConstructor +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + @Column(name = "name", nullable = false) + private String name; + @Column(name = "email", nullable = false) + private String email; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/UserRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/UserRepository.java new file mode 100644 index 0000000..5887768 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/UserRepository.java @@ -0,0 +1,7 @@ +package ru.practicum.evmsevice.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.evmsevice.model.User; + +public interface UserRepository extends JpaRepository { +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminUserService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminUserService.java new file mode 100644 index 0000000..ac1b526 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminUserService.java @@ -0,0 +1,15 @@ +package ru.practicum.evmsevice.service; + +import ru.practicum.evmsevice.model.User; + +import java.util.List; + +public interface AdminUserService { + List getUsers(); + + User getUserById(Integer id); + + User addUser(User user); + + void deleteUser(Integer id); +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminUserServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminUserServiceImpl.java new file mode 100644 index 0000000..e8e9cd6 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminUserServiceImpl.java @@ -0,0 +1,44 @@ +package ru.practicum.evmsevice.service; + +import org.springframework.stereotype.Service; +import ru.practicum.evmsevice.exception.NotFoundException; +import ru.practicum.evmsevice.model.User; +import ru.practicum.evmsevice.repository.UserRepository; + +import java.util.List; + +@Service +public class AdminUserServiceImpl implements AdminUserService { + private final UserRepository userRepository; + + public AdminUserServiceImpl(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public User addUser(User user) { + User savedUser = userRepository.save(user); + return savedUser; + } + + @Override + public List getUsers() { + return userRepository.findAll(); + } + + @Override + public User getUserById(Integer id) { + User user = userRepository.findById(id) + .orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id)); + return user; + } + + @Override + public void deleteUser(Integer id) { + User user = userRepository.findById(id) + .orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id)); + userRepository.deleteById(id); + } +} diff --git a/ewm-service/src/main/resources/application.properties b/ewm-service/src/main/resources/application.properties index 00ac254..7bb57dd 100644 --- a/ewm-service/src/main/resources/application.properties +++ b/ewm-service/src/main/resources/application.properties @@ -1,9 +1,10 @@ server.port=8080 +spring.application.name=ewm-service spring.sql.init.mode=always spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.datasource.driverClassName=org.postgresql.Driver -spring.datasource.url=jdbc:postgresql://192.168.0.102:5432/ewmdb +spring.datasource.url=jdbc:postgresql://192.168.0.102:5434/ewmdb spring.datasource.username=ewmdb spring.datasource.password=ewmdb diff --git a/ewm-service/src/main/resources/schema.sql b/ewm-service/src/main/resources/schema.sql index 02a763b..bbe528f 100644 --- a/ewm-service/src/main/resources/schema.sql +++ b/ewm-service/src/main/resources/schema.sql @@ -1,56 +1,58 @@ -CREATE TABLE "categorys" ( - "id" integer PRIMARY KEY, - "name" varchar +CREATE TABLE IF NOT EXISTS "categorys" ( + "id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, + "name" VARCHAR(128) NOT NULL, + CONSTRAINT pk_category PRIMARY KEY (id), + CONSTRAINT UQ_CATEGORY UNIQUE (name) ); -CREATE TABLE "users" ( - "id" integer PRIMARY KEY, - "username" varchar, - "email" varchar +CREATE TABLE IF NOT EXISTS "users" ( + "id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, + name VARCHAR(128) NOT NULL, + email VARCHAR(128) NOT NULL, + CONSTRAINT pk_user PRIMARY KEY (id), + CONSTRAINT UQ_USER_EMAIL UNIQUE (email) ); -CREATE TABLE "events" ( - "id" integer PRIMARY KEY, - "category_id" integer, - "user_id" integer NOT NULL, - "title" varchar, - "annotation" varchar, - "description" varchar, - "eventDate" timestamp, - "location_lat" float, - "location_lon" float, - "paid" bool, - "participantLimit" integer, - "requestModeration" bool +CREATE TABLE IF NOT EXISTS "events" ( + "id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, + "category_id" INTEGER, + "user_id" INTEGER NOT NULL, + "title" VARCHAR(128), + "annotation" VARCHAR(256), + "description" VARCHAR(256), + "eventDate" TIMESTAMP WITHOUT TIME ZONE, + "location_lat" FLOAT, + "location_lon" FLOAT, + "paid" BOOLEAN, + "participantLimit" INTEGER, + "requestModeration" BOOLEAN, + CONSTRAINT pk_event PRIMARY KEY (id), + CONSTRAINT fk_events_to_users FOREIGN KEY (user_id) REFERENCES users (id), + CONSTRAINT fk_events_to_categorys FOREIGN KEY (category_id) REFERENCES categorys (id) ); -CREATE TABLE "requests" ( - "id" integer PRIMARY KEY, - "event" integer, - "requester" integer, - "status" VARCHAR, - "created" timestamp +CREATE TABLE IF NOT EXISTS "requests" ( + "id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, + "requester_id" INTEGER NOT NULL, + "event_id" INTEGER NOT NULL, + "status" VARCHAR(32), + "created" TIMESTAMP WITHOUT TIME ZONE, + CONSTRAINT pk_request PRIMARY KEY (id), + CONSTRAINT fk_requests_to_users FOREIGN KEY (requester_id) REFERENCES users (id), + CONSTRAINT fk_requests_to_events FOREIGN KEY (event_id) REFERENCES events (id) ); -CREATE TABLE "compilations" ( - "id" integer PRIMARY KEY, - "title" varchar, - "pinned" bool +CREATE TABLE IF NOT EXISTS "compilations" ( + "id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, + "title" VARCHAR(128), + "pinned" BOOLEAN, + CONSTRAINT pk_compilation PRIMARY KEY (id) ); -CREATE TABLE "eventlinks" ( - "event_id" integer, - "compiation_id" integer +CREATE TABLE IF NOT EXISTS "eventlinks" ( + "event_id" INTEGER NOT NULL, + "compiation_id" INTEGER NOT NULL, + CONSTRAINT pk_eventlinks PRIMARY KEY (event_id, compiation_id), + CONSTRAINT fk_links_to_events FOREIGN KEY (event_id) REFERENCES events (id), + CONSTRAINT fk_links_to_compilations FOREIGN KEY (compiation_id) REFERENCES compilations (id) ); - -ALTER TABLE "events" ADD CONSTRAINT "user_events" FOREIGN KEY ("user_id") REFERENCES "users" ("id"); - -ALTER TABLE "events" ADD CONSTRAINT "category_events" FOREIGN KEY ("category_id") REFERENCES "categorys" ("id"); - -ALTER TABLE "requests" ADD CONSTRAINT "request_events" FOREIGN KEY ("id") REFERENCES "events" ("id"); - -ALTER TABLE "requests" ADD CONSTRAINT "user_requests" FOREIGN KEY ("id") REFERENCES "users" ("id"); - -ALTER TABLE "eventlinks" ADD CONSTRAINT "events_links" FOREIGN KEY ("event_id") REFERENCES "events" ("id"); - -ALTER TABLE "eventlinks" ADD CONSTRAINT "compilation_links" FOREIGN KEY ("compiation_id") REFERENCES "compilations" ("id"); From 618de419f5d78cf0cd33ae7d10f7cc0ff6b14145 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Sun, 25 May 2025 22:55:20 +0700 Subject: [PATCH 03/29] feat: ErrorAdvisor.java --- .../controller/AdminUserController.java | 1 - .../evmsevice/controller/ArrorAdvisor.java | 34 +++++++++++++++++++ .../ru/practicum/evmsevice/dto/ApiError.java | 5 ++- 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/controller/ArrorAdvisor.java diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminUserController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminUserController.java index f4bb0fb..3e8b597 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminUserController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminUserController.java @@ -29,7 +29,6 @@ public class AdminUserController { @Value("${spring.application.name}") private String appName; - private static final DateTimeFormatter DATA_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private final StatsClient statsClient; private final AdminUserService adminUserService; diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ArrorAdvisor.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ArrorAdvisor.java new file mode 100644 index 0000000..8910c1e --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ArrorAdvisor.java @@ -0,0 +1,34 @@ +package ru.practicum.evmsevice.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.client.HttpClientErrorException; +import ru.practicum.evmsevice.dto.ApiError; +import ru.practicum.evmsevice.exception.NotFoundException; +import ru.practicum.statdto.ErrorMessage; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * Класс обработки исключений при обработке поступивших http запросов + */ +@Slf4j +@RestControllerAdvice +public class ArrorAdvisor { + + @ExceptionHandler(NotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ApiError notFoundObject(NotFoundException exception) { + log.error("404 {}.", exception.getMessage()); + ApiError apiError = new ApiError(); + apiError.setStatus(HttpStatus.NOT_FOUND); + apiError.setReason("Запрошенный объект не найден."); + apiError.setMessage(exception.getMessage()); + apiError.setTimestamp(LocalDateTime.now()); + return apiError; + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/ApiError.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/ApiError.java index 1def575..cf7383c 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/ApiError.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/ApiError.java @@ -1,8 +1,10 @@ package ru.practicum.evmsevice.dto; +import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.springframework.http.HttpStatus; import java.time.LocalDateTime; import java.util.ArrayList; @@ -12,9 +14,10 @@ @Getter @NoArgsConstructor public class ApiError { - private String status; + private HttpStatus status; private String reason; private String message; private LocalDateTime timestamp; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private List errors = new ArrayList<>(); } From 73272f63f3b3b9ae973c87924bed4872a7a29dc0 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Mon, 26 May 2025 20:54:26 +0700 Subject: [PATCH 04/29] fix: ErrorAdvisor.java --- .../evmsevice/controller/ArrorAdvisor.java | 34 ----- .../evmsevice/controller/ErrorAdvisor.java | 117 ++++++++++++++++++ .../ru/practicum/evmsevice/dto/ApiError.java | 4 +- .../ru/practicum/evmsevice/dto/UserDto.java | 6 +- .../practicum/evmsevice/dto/UserShortDto.java | 6 +- 5 files changed, 124 insertions(+), 43 deletions(-) delete mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/controller/ArrorAdvisor.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ArrorAdvisor.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ArrorAdvisor.java deleted file mode 100644 index 8910c1e..0000000 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ArrorAdvisor.java +++ /dev/null @@ -1,34 +0,0 @@ -package ru.practicum.evmsevice.controller; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.client.HttpClientErrorException; -import ru.practicum.evmsevice.dto.ApiError; -import ru.practicum.evmsevice.exception.NotFoundException; -import ru.practicum.statdto.ErrorMessage; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; - -/** - * Класс обработки исключений при обработке поступивших http запросов - */ -@Slf4j -@RestControllerAdvice -public class ArrorAdvisor { - - @ExceptionHandler(NotFoundException.class) - @ResponseStatus(HttpStatus.NOT_FOUND) - public ApiError notFoundObject(NotFoundException exception) { - log.error("404 {}.", exception.getMessage()); - ApiError apiError = new ApiError(); - apiError.setStatus(HttpStatus.NOT_FOUND); - apiError.setReason("Запрошенный объект не найден."); - apiError.setMessage(exception.getMessage()); - apiError.setTimestamp(LocalDateTime.now()); - return apiError; - } -} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java new file mode 100644 index 0000000..293f934 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java @@ -0,0 +1,117 @@ +package ru.practicum.evmsevice.controller; + +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import ru.practicum.evmsevice.dto.ApiError; +import ru.practicum.evmsevice.exception.InternalServerException; +import ru.practicum.evmsevice.exception.NotFoundException; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Класс обработки исключений при обработке поступивших http запросов + */ +@Slf4j +@RestControllerAdvice +public class ErrorAdvisor { + + @ExceptionHandler(NotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ApiError notFoundObject(NotFoundException exception) { + log.error("404 {}.", exception.getMessage()); + ApiError apiError = new ApiError(); + apiError.setStatus(HttpStatus.NOT_FOUND); + apiError.setReason("Запрошенный объект не найден."); + apiError.setMessage(exception.getMessage()); + apiError.setTimestamp(LocalDateTime.now()); + return apiError; + } + + @ExceptionHandler(InternalServerException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiError onInternalException(final InternalServerException e) { + log.error("500 {}", e.getMessage()); + ApiError apiError = new ApiError(); + apiError.setStatus(HttpStatus.INTERNAL_SERVER_ERROR); + apiError.setReason("Внутренняя ошибка сервера."); + apiError.setMessage(e.getMessage()); + apiError.setTimestamp(LocalDateTime.now()); + return apiError; + } + + + @ExceptionHandler(DataIntegrityViolationException.class) + @ResponseStatus(HttpStatus.CONFLICT) + public ApiError onDataIntegrityViolationException(final DataIntegrityViolationException e) { + log.error("409 {}", e.getMessage()); + ApiError apiError = new ApiError(); + apiError.setStatus(HttpStatus.CONFLICT); + apiError.setReason(e.getRootCause().getMessage()); + apiError.setMessage(e.getMessage()); + apiError.setTimestamp(LocalDateTime.now()); + return apiError; + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiError onMethodArgumentNotValidException(MethodArgumentNotValidException e) { + log.error("400 {}.", e.getMessage()); + final List violations = e.getBindingResult().getFieldErrors().stream() + .map(error -> String.format("Field: %s. Error: %s. Value: \'%s\'. ", + error.getField(), + error.getDefaultMessage(), + error.getRejectedValue() + )) + .collect(Collectors.toList()); + + ApiError apiError = new ApiError(); + apiError.setStatus(HttpStatus.BAD_REQUEST); + apiError.setReason("Запрос сформирован некорректно."); + apiError.setMessage(violations.stream().collect(Collectors.joining())); + apiError.setTimestamp(LocalDateTime.now()); + return apiError; + } + + @ExceptionHandler({ConstraintViolationException.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiError onConstraintValidationException(ConstraintViolationException e) { + log.error("400 {}.", e.getMessage()); + final List violations = e.getConstraintViolations().stream() + .map( + violation -> String.format("Field: %s. Error: %s. Value: \'%s\'. ", + violation.getPropertyPath().toString(), + violation.getMessage(), + violation.getInvalidValue() + )) + .collect(Collectors.toList()); + + ApiError apiError = new ApiError(); + apiError.setStatus(HttpStatus.BAD_REQUEST); + apiError.setReason("Запрос сформирован некорректно."); + apiError.setMessage(violations.stream().collect(Collectors.joining())); + apiError.setTimestamp(LocalDateTime.now()); + + return apiError; + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiError handleException(final Exception e) { + log.error("Error", e); + ApiError apiError = new ApiError(); + apiError.setStatus(HttpStatus.INTERNAL_SERVER_ERROR); + apiError.setReason(e.getCause().getMessage()); + apiError.setMessage(e.getMessage()); + apiError.setTimestamp(LocalDateTime.now()); + return apiError; + } + +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/ApiError.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/ApiError.java index cf7383c..6da48c6 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/ApiError.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/ApiError.java @@ -1,6 +1,7 @@ package ru.practicum.evmsevice.dto; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -17,7 +18,8 @@ public class ApiError { private HttpStatus status; private String reason; private String message; - private LocalDateTime timestamp; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime timestamp; + @JsonIgnore private List errors = new ArrayList<>(); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UserDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UserDto.java index 06d509e..4f30d32 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UserDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UserDto.java @@ -2,15 +2,13 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; @Setter @Getter @NoArgsConstructor @AllArgsConstructor +@ToString public class UserDto { private Integer id; @NotBlank(message = "Имя не может быть пустым") diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UserShortDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UserShortDto.java index 1bc655f..f610a95 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UserShortDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UserShortDto.java @@ -1,15 +1,13 @@ package ru.practicum.evmsevice.dto; import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; @Setter @Getter @NoArgsConstructor @AllArgsConstructor +@ToString public class UserShortDto { private Integer id; @NotBlank(message = "Имя не может быть пустым") From 8a5b7d11f25cc23d9d9e23acdb7aa22812f9a165 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Mon, 26 May 2025 22:23:11 +0700 Subject: [PATCH 05/29] feat: /admin/categories --- docker-compose.yml | 7 ++- .../controller/AdminCategoryController.java | 53 +++++++++++++++++++ .../practicum/evmsevice/model/Category.java | 20 +++++++ .../ru/practicum/evmsevice/model/User.java | 5 +- .../repository/CategoryRepository.java | 7 +++ .../service/AdminCategoryService.java | 9 ++++ .../service/AdminCategoryServiceImpl.java | 36 +++++++++++++ 7 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminCategoryController.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/model/Category.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/repository/CategoryRepository.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminCategoryService.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminCategoryServiceImpl.java diff --git a/docker-compose.yml b/docker-compose.yml index 6a9811d..4af02be 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,11 @@ -version: '3.1' services: stats-db: image: postgres:16.1 container_name: postgres-stat ports: - "5432:5432" + volumes: + - ./volumes/stat/postgres:/var/lib/postgresql/data/ environment: - POSTGRES_PASSWORD=statdb - POSTGRES_USER=statdb @@ -44,7 +45,9 @@ services: image: postgres:16.1 container_name: postgres-ewm ports: - - "5434:5434" + - "5434:5432" + volumes: + - ./volumes/ewm/postgres:/var/lib/postgresql/data/ environment: - POSTGRES_PASSWORD=ewmdb - POSTGRES_USER=ewmdb diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminCategoryController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminCategoryController.java new file mode 100644 index 0000000..99a8130 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminCategoryController.java @@ -0,0 +1,53 @@ +package ru.practicum.evmsevice.controller; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.evmsevice.client.StatsClient; +import ru.practicum.evmsevice.model.Category; +import ru.practicum.evmsevice.service.AdminCategoryService; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping("/admin/categories") +public class AdminCategoryController { + @Value("${spring.application.name}") + private String appName; + private final StatsClient statsClient; + private final AdminCategoryService adminCategoryService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public Category createCategory(@Validated @RequestBody Category category, + HttpServletRequest request) { + log.info("Создаем категорию {}.", category.getName()); + statsClient.hitInfo(appName, request); + Category newCategory = adminCategoryService.createCategory(category); + return newCategory; + } + + @PatchMapping("/{id}") + @ResponseStatus(HttpStatus.OK) + public Category updateCategory(@Validated @RequestBody Category category, + @PathVariable int id, + HttpServletRequest request) { + log.info("Обновляем категорию id={}.", id); + statsClient.hitInfo(appName, request); + category.setId(id); + Category updatedCategory = adminCategoryService.updateCategory(category); + return updatedCategory; + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.OK) + public void deletecategory(@PathVariable int id, HttpServletRequest request) { + log.info("Удаляем категорию id={}.", id); + statsClient.hitInfo(appName, request); + adminCategoryService.deleteCategory(id); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Category.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Category.java new file mode 100644 index 0000000..34dfdc2 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Category.java @@ -0,0 +1,20 @@ +package ru.practicum.evmsevice.model; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import lombok.*; + +@Entity +@Setter +@Getter +@Table(name = "category", schema = "public") +@EqualsAndHashCode(of = {"name"}) +@NoArgsConstructor +public class Category { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + @Column(name = "name", nullable = false) + @NotBlank(message = "Название категории не может быть пустым") + private String name; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/User.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/User.java index 390b7a7..6726fc0 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/model/User.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/User.java @@ -1,10 +1,7 @@ package ru.practicum.evmsevice.model; import jakarta.persistence.*; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; /** * Класс описания пользователя diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CategoryRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CategoryRepository.java new file mode 100644 index 0000000..0775f42 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CategoryRepository.java @@ -0,0 +1,7 @@ +package ru.practicum.evmsevice.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.evmsevice.model.Category; + +public interface CategoryRepository extends JpaRepository { +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminCategoryService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminCategoryService.java new file mode 100644 index 0000000..5db2798 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminCategoryService.java @@ -0,0 +1,9 @@ +package ru.practicum.evmsevice.service; + +import ru.practicum.evmsevice.model.Category; + +public interface AdminCategoryService { + Category createCategory(Category category); + Category updateCategory(Category category); + void deleteCategory(Integer id); +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminCategoryServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminCategoryServiceImpl.java new file mode 100644 index 0000000..3f2d472 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminCategoryServiceImpl.java @@ -0,0 +1,36 @@ +package ru.practicum.evmsevice.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.evmsevice.exception.NotFoundException; +import ru.practicum.evmsevice.model.Category; +import ru.practicum.evmsevice.repository.CategoryRepository; + +@Service +@Transactional +@RequiredArgsConstructor +public class AdminCategoryServiceImpl implements AdminCategoryService { + private final CategoryRepository categoryRepository; + + @Override + public Category createCategory(Category category) { + Category savedCategory = categoryRepository.save(category); + return savedCategory; + } + + @Override + public Category updateCategory(Category category) { + categoryRepository.findById(category.getId()) + .orElseThrow(() -> new NotFoundException("Не найдена категория id=" + category.getId())); + Category updatedCategory = categoryRepository.save(category); + return updatedCategory; + } + + @Override + public void deleteCategory(Integer id) { + categoryRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Не найдена категория id=" + id)); + categoryRepository.deleteById(id); + } +} From 07a2de35d62630b1a02e42276f82159da963f0c0 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Tue, 27 May 2025 22:13:17 +0700 Subject: [PATCH 06/29] =?UTF-8?q?fix:=20/admin/categories=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20DTO=20=D0=BA=D0=BB?= =?UTF-8?q?=D0=B0=D1=81=D1=81=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ewm-service/schema.png | Bin 55245 -> 62186 bytes .../controller/AdminCategoryController.java | 18 +++-- .../practicum/evmsevice/dto/CategoryDto.java | 17 +++++ .../evmsevice/dto/NewCategoryDto.java | 16 ++++ .../evmsevice/mapper/CategoryMapper.java | 22 ++++++ .../practicum/evmsevice/model/Category.java | 2 +- ewm-service/src/main/resources/schema.sql | 71 ++++++++++-------- 7 files changed, 106 insertions(+), 40 deletions(-) create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/dto/CategoryDto.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewCategoryDto.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CategoryMapper.java diff --git a/ewm-service/schema.png b/ewm-service/schema.png index 269b05a01c0bdec93399ee578647387104231c02..fdde26e7f031028631c38bd4458030fb4591b56f 100644 GIT binary patch literal 62186 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 literal 55245 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 Date: Wed, 28 May 2025 23:03:19 +0700 Subject: [PATCH 07/29] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=20event.java?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ru/practicum/evmsevice/model/Event.java | 51 +++++++++++++++++++ ewm-service/src/main/resources/schema.sql | 23 ++++----- 2 files changed, 61 insertions(+), 13 deletions(-) create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java new file mode 100644 index 0000000..ba82354 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java @@ -0,0 +1,51 @@ +package ru.practicum.evmsevice.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Setter +@Getter +@Table(name = "eventss", schema = "public") +@NoArgsConstructor +public class Event { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + @Column(name = "annotation", nullable = false) + private String annotation; + @ManyToOne + @JoinColumn(name = "category_id") + private Category category; + @Column(name = "confirmedrequests") + private Integer confirmedRequests; + @Column(name = "createdon", nullable = false) + private LocalDateTime createdon; + @Column(name = "description") + private String description; + @Column(name = "eventdate", nullable = false) + private LocalDateTime eventDate; + @ManyToOne + @JoinColumn(name = "user_id") + private User initiator; + @Column(name = "lat") + private Float lat; + @Column(name = "lon") + private Float lon; + @Column(name = "paid") + private Boolean paid; + @Column(name = "participantlimit") + private Integer participantLimit; + @Column(name = "publishedon") + private LocalDateTime publishedOn; + @Column(name = "requestmoderation") + private Boolean requestModeration; + @Column(name = "state") + private String state; + @Column(name = "title") + private String title; +} diff --git a/ewm-service/src/main/resources/schema.sql b/ewm-service/src/main/resources/schema.sql index c618d91..9f63d24 100644 --- a/ewm-service/src/main/resources/schema.sql +++ b/ewm-service/src/main/resources/schema.sql @@ -13,29 +13,26 @@ CREATE TABLE IF NOT EXISTS users ( CONSTRAINT UQ_USER_EMAIL UNIQUE (email) ); -CREATE TABLE IF NOT EXISTS locations ( - id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, - lat FLOAT NOT NULL, - lon FLOAT NOT NULL, - CONSTRAINT pk_location PRIMARY KEY (id) -); - CREATE TABLE IF NOT EXISTS events ( id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, - category_id INTEGER, - user_id INTEGER NOT NULL, - title VARCHAR(128), annotation VARCHAR(256), + category_id INTEGER, + confirmedRequests INTEGER, + createdOn TIMESTAMP WITHOUT TIME ZONE, description VARCHAR(256), eventDate TIMESTAMP WITHOUT TIME ZONE, - location_id INTEGER, + user_id INTEGER NOT NULL, + lat FLOAT, + lon FLOAT, paid BOOLEAN, participantLimit INTEGER, + publishedOn TIMESTAMP WITHOUT TIME ZONE, requestModeration BOOLEAN, + state VARCHAR(32), + title VARCHAR(128), CONSTRAINT pk_event PRIMARY KEY (id), CONSTRAINT fk_events_to_users FOREIGN KEY (user_id) REFERENCES users (id), - CONSTRAINT fk_events_to_categories FOREIGN KEY (category_id) REFERENCES categories (id), - CONSTRAINT fk_events_to_locations FOREIGN KEY (location_id) REFERENCES locations (id) + CONSTRAINT fk_events_to_categories FOREIGN KEY (category_id) REFERENCES categories (id) ); CREATE TABLE IF NOT EXISTS requests ( From 9abd3eff6fffe4ac83d08792ec0847318c801b78 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Thu, 29 May 2025 22:03:54 +0700 Subject: [PATCH 08/29] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=80=D0=B5=D0=BF=D0=BE=D0=B7=D0=B8=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=B9=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9?= =?UTF-8?q?=20=D0=B8=20DTO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../practicum/evmsevice/dto/EventFullDto.java | 36 +++++++++++++++++++ .../evmsevice/dto/EventShortDto.java | 24 +++++++++++++ .../practicum/evmsevice/dto/NewEventDto.java | 30 ++++++++++++++++ .../evmsevice/mapper/EventMapper.java | 26 ++++++++++++++ .../ru/practicum/evmsevice/model/Event.java | 2 +- .../practicum/evmsevice/model/EventState.java | 18 ++++++++++ .../practicum/evmsevice/model/Location.java | 15 ++++++++ .../evmsevice/repository/EventRepository.java | 7 ++++ 8 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventFullDto.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventShortDto.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewEventDto.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/model/EventState.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/model/Location.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventFullDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventFullDto.java new file mode 100644 index 0000000..e3f89de --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventFullDto.java @@ -0,0 +1,36 @@ +package ru.practicum.evmsevice.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.evmsevice.model.Location; + +import java.time.LocalDateTime; + +@Setter +@Getter +@NoArgsConstructor +public class EventFullDto { + private String annotation; + private CategoryDto category; + private Integer confirmedRequests; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdOn; + private String description; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime eventDate; + private Integer id; + private UserShortDto initiator; + private Location location; + private Boolean paid; + private Integer participantLimit; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime publishedOn; + private Boolean requestModeration; + private String state; + @Size(min = 3, max = 120, message = "длина заголовка 3 - 120 символов.") + private String title; + private Integer views; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventShortDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventShortDto.java new file mode 100644 index 0000000..92a02d6 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventShortDto.java @@ -0,0 +1,24 @@ +package ru.practicum.evmsevice.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Setter +@Getter +@ToString +@NoArgsConstructor +public class EventShortDto { + private Integer id; + private String annotation; + private CategoryDto category; + private Integer confirmedRequest; + private LocalDateTime eventDate; + private UserShortDto initiator; + private Boolean paid; + private String title; + private Integer views; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewEventDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewEventDto.java new file mode 100644 index 0000000..2e353f1 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewEventDto.java @@ -0,0 +1,30 @@ +package ru.practicum.evmsevice.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.evmsevice.model.Location; + +import java.time.LocalDateTime; + +@Setter +@Getter +@NoArgsConstructor +public class NewEventDto +{ + @Size(min = 20, max = 2000, message = "длина аннотации 20 - 2000 символов.") + private String annotation; + private Integer category; + @Size(min = 20, max = 7000, message = "длина описания 20 - 7000 символов.") + private String description; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime eventDate; + private Location location; + private Boolean paid; + private Integer participantLimit; + private Boolean requestModeration; + @Size(min = 3, max = 120, message = "длина заголовка 3 - 120 символов.") + private String title; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java new file mode 100644 index 0000000..4e0983e --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java @@ -0,0 +1,26 @@ +package ru.practicum.evmsevice.mapper; + +import ru.practicum.evmsevice.dto.NewEventDto; +import ru.practicum.evmsevice.model.Event; +import ru.practicum.evmsevice.model.EventState; + +import java.time.LocalDateTime; + +public class EventMapper { + private EventMapper() {} + + public static Event mapNewEvent(final NewEventDto newDto) { + Event event = new Event(); + event.setAnnotation(newDto.getAnnotation()); + event.setDescription(newDto.getDescription()); + event.setEventDate(newDto.getEventDate()); + event.setCreatedOn(LocalDateTime.now()); + event.setLat(newDto.getLocation().getLat()); + event.setLon(newDto.getLocation().getLon()); + event.setPaid(newDto.getPaid()); + event.setParticipantLimit(newDto.getParticipantLimit()); + event.setRequestModeration(newDto.getRequestModeration()); + event.setState(EventState.PENDING.name()); + return event; + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java index ba82354..b03df9d 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java @@ -24,7 +24,7 @@ public class Event { @Column(name = "confirmedrequests") private Integer confirmedRequests; @Column(name = "createdon", nullable = false) - private LocalDateTime createdon; + private LocalDateTime createdOn; @Column(name = "description") private String description; @Column(name = "eventdate", nullable = false) diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/EventState.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/EventState.java new file mode 100644 index 0000000..0d866d0 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/EventState.java @@ -0,0 +1,18 @@ +package ru.practicum.evmsevice.model; + +import java.util.Optional; + +public enum EventState { + PENDING, + PUBLISHED, + CANCELED; + + public static Optional from(String state) { + for (EventState eventState : EventState.values()) { + if (eventState.name().equalsIgnoreCase(state)) { + return Optional.of(eventState); + } + } + return Optional.empty(); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Location.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Location.java new file mode 100644 index 0000000..e5501ad --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Location.java @@ -0,0 +1,15 @@ +package ru.practicum.evmsevice.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class Location { + private Float lat; + private Float lon; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java new file mode 100644 index 0000000..15f3ca3 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java @@ -0,0 +1,7 @@ +package ru.practicum.evmsevice.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.evmsevice.model.Event; + +public interface EventRepository extends JpaRepository { +} From 10759e2addcb3c3637badc293600b87088fdf8c0 Mon Sep 17 00:00:00 2001 From: andrej1307 Date: Fri, 30 May 2025 18:51:54 +0700 Subject: [PATCH 09/29] feat: EventService.java, UserController.java --- .../controller/AdminCategoryController.java | 10 ++-- .../controller/AdminUserController.java | 16 +++--- .../evmsevice/controller/UserController.java | 27 ++++++++++ .../practicum/evmsevice/dto/NewEventDto.java | 5 ++ .../evmsevice/mapper/EventMapper.java | 37 ++++++++++++- .../repository/CategoryRepository.java | 3 ++ ...egoryService.java => CategoryService.java} | 3 +- ...viceImpl.java => CategoryServiceImpl.java} | 9 +++- .../evmsevice/service/EventService.java | 17 ++++++ .../evmsevice/service/EventServiceImpl.java | 52 +++++++++++++++++++ ...AdminUserService.java => UserService.java} | 2 +- ...rServiceImpl.java => UserServiceImpl.java} | 4 +- .../src/main/resources/application.properties | 14 ++--- 13 files changed, 171 insertions(+), 28 deletions(-) create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java rename ewm-service/src/main/java/ru/practicum/evmsevice/service/{AdminCategoryService.java => CategoryService.java} (74%) rename ewm-service/src/main/java/ru/practicum/evmsevice/service/{AdminCategoryServiceImpl.java => CategoryServiceImpl.java} (78%) create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java rename ewm-service/src/main/java/ru/practicum/evmsevice/service/{AdminUserService.java => UserService.java} (86%) rename ewm-service/src/main/java/ru/practicum/evmsevice/service/{AdminUserServiceImpl.java => UserServiceImpl.java} (90%) diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminCategoryController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminCategoryController.java index 7fb6be1..a1076f9 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminCategoryController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminCategoryController.java @@ -12,7 +12,7 @@ import ru.practicum.evmsevice.dto.NewCategoryDto; import ru.practicum.evmsevice.mapper.CategoryMapper; import ru.practicum.evmsevice.model.Category; -import ru.practicum.evmsevice.service.AdminCategoryService; +import ru.practicum.evmsevice.service.CategoryService; @Slf4j @RequiredArgsConstructor @@ -22,7 +22,7 @@ public class AdminCategoryController { @Value("${spring.application.name}") private String appName; private final StatsClient statsClient; - private final AdminCategoryService adminCategoryService; + private final CategoryService categoryService; @PostMapping @ResponseStatus(HttpStatus.CREATED) @@ -30,7 +30,7 @@ public CategoryDto createCategory(@Validated @RequestBody NewCategoryDto categor HttpServletRequest request) { log.info("Создаем категорию {}.", categoryDto.getName()); statsClient.hitInfo(appName, request); - Category newCategory = adminCategoryService.createCategory(CategoryMapper.toCategory(categoryDto)); + Category newCategory = categoryService.createCategory(CategoryMapper.toCategory(categoryDto)); return CategoryMapper.toDto(newCategory); } @@ -43,7 +43,7 @@ public CategoryDto updateCategory(@Validated @RequestBody NewCategoryDto categor statsClient.hitInfo(appName, request); Category category = CategoryMapper.toCategory(categoryDto); category.setId(id); - Category updatedCategory = adminCategoryService.updateCategory(category); + Category updatedCategory = categoryService.updateCategory(category); return CategoryMapper.toDto(updatedCategory); } @@ -52,6 +52,6 @@ public CategoryDto updateCategory(@Validated @RequestBody NewCategoryDto categor public void deletecategory(@PathVariable int id, HttpServletRequest request) { log.info("Удаляем категорию id={}.", id); statsClient.hitInfo(appName, request); - adminCategoryService.deleteCategory(id); + categoryService.deleteCategory(id); } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminUserController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminUserController.java index 3e8b597..99f5686 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminUserController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminUserController.java @@ -11,12 +11,8 @@ import ru.practicum.evmsevice.dto.UserDto; import ru.practicum.evmsevice.mapper.UserMapper; import ru.practicum.evmsevice.model.User; -import ru.practicum.evmsevice.repository.UserRepository; -import ru.practicum.evmsevice.service.AdminUserService; -import ru.practicum.statdto.HitDto; +import ru.practicum.evmsevice.service.UserService; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.List; /** @@ -30,14 +26,14 @@ public class AdminUserController { @Value("${spring.application.name}") private String appName; private final StatsClient statsClient; - private final AdminUserService adminUserService; + private final UserService userService; @GetMapping @ResponseStatus(HttpStatus.OK) public List getUsers(HttpServletRequest request) { log.info("{} запрашивает список пользователей.", request.getRemoteUser()); statsClient.hitInfo(appName, request); - return adminUserService.getUsers().stream() + return userService.getUsers().stream() .map(UserMapper::toUserDto) .toList(); } @@ -47,7 +43,7 @@ public List getUsers(HttpServletRequest request) { public UserDto getUser(HttpServletRequest request, @PathVariable Integer id) { log.info("Выполняем поиск пользователя id={}.", id); statsClient.hitInfo(appName, request); - User user = adminUserService.getUserById(id); + User user = userService.getUserById(id); return UserMapper.toUserDto(user); } @@ -56,7 +52,7 @@ public UserDto getUser(HttpServletRequest request, @PathVariable Integer id) { public UserDto createUser(@Validated @RequestBody UserDto userDto, HttpServletRequest request) { log.info("Создаем нового пользователя {}", userDto.toString()); statsClient.hitInfo(appName, request); - User savedUser =adminUserService.addUser(UserMapper.toUser(userDto)); + User savedUser = userService.addUser(UserMapper.toUser(userDto)); return UserMapper.toUserDto(savedUser); } @@ -65,6 +61,6 @@ public UserDto createUser(@Validated @RequestBody UserDto userDto, HttpServletRe public void deleteUser(HttpServletRequest request, @PathVariable Integer id) { log.info("Удаляем пользователя {}", id); statsClient.hitInfo(appName, request); - adminUserService.deleteUser(id); + userService.deleteUser(id); } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java new file mode 100644 index 0000000..f2fe435 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java @@ -0,0 +1,27 @@ +package ru.practicum.evmsevice.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.evmsevice.dto.EventFullDto; +import ru.practicum.evmsevice.dto.NewEventDto; +import ru.practicum.evmsevice.model.Event; +import ru.practicum.evmsevice.service.EventService; +import ru.practicum.evmsevice.service.UserService; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping("/users")public class UserController { + private final EventService eventService; + + @PostMapping("/{id}/events") + @ResponseStatus(HttpStatus.CREATED) + public EventFullDto createEvent(@PathVariable int id, + @Validated @RequestBody NewEventDto eventDto) { + log.info("Creating new event {}", eventDto); + return eventService.createEvent(eventDto, id); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewEventDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewEventDto.java index 2e353f1..a8c0b3d 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewEventDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewEventDto.java @@ -1,10 +1,13 @@ package ru.practicum.evmsevice.dto; import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.ToString; import ru.practicum.evmsevice.model.Location; import java.time.LocalDateTime; @@ -12,10 +15,12 @@ @Setter @Getter @NoArgsConstructor +@ToString public class NewEventDto { @Size(min = 20, max = 2000, message = "длина аннотации 20 - 2000 символов.") private String annotation; + @NotNull(message = "Категоря должна быть определена") private Integer category; @Size(min = 20, max = 7000, message = "длина описания 20 - 7000 символов.") private String description; diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java index 4e0983e..61ec177 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java @@ -1,15 +1,18 @@ package ru.practicum.evmsevice.mapper; +import ru.practicum.evmsevice.dto.EventFullDto; +import ru.practicum.evmsevice.dto.EventShortDto; import ru.practicum.evmsevice.dto.NewEventDto; import ru.practicum.evmsevice.model.Event; import ru.practicum.evmsevice.model.EventState; +import ru.practicum.evmsevice.model.Location; import java.time.LocalDateTime; public class EventMapper { private EventMapper() {} - public static Event mapNewEvent(final NewEventDto newDto) { + public static Event toEvent(final NewEventDto newDto) { Event event = new Event(); event.setAnnotation(newDto.getAnnotation()); event.setDescription(newDto.getDescription()); @@ -21,6 +24,38 @@ public static Event mapNewEvent(final NewEventDto newDto) { event.setParticipantLimit(newDto.getParticipantLimit()); event.setRequestModeration(newDto.getRequestModeration()); event.setState(EventState.PENDING.name()); + event.setTitle(newDto.getTitle()); return event; } + + public static EventFullDto toFullDto(Event event) { + EventFullDto dto = new EventFullDto(); + dto.setId(event.getId()); + dto.setAnnotation(event.getAnnotation()); + dto.setDescription(event.getDescription()); + dto.setEventDate(event.getEventDate()); + dto.setCreatedOn(event.getCreatedOn()); + dto.setCategory(CategoryMapper.toDto(event.getCategory())); + dto.setInitiator(UserMapper.toUserShortDto(event.getInitiator())); + dto.setLocation(new Location(event.getLat(), event.getLon())); + dto.setParticipantLimit(event.getParticipantLimit()); + dto.setRequestModeration(event.getRequestModeration()); + dto.setState(event.getState()); + dto.setPaid(event.getPaid()); + dto.setPublishedOn(event.getPublishedOn()); + dto.setTitle(event.getTitle()); + return dto; + } + + public static EventShortDto toShortDto(Event event) { + EventShortDto dto = new EventShortDto(); + dto.setId(event.getId()); + dto.setAnnotation(event.getAnnotation()); + dto.setCategory(CategoryMapper.toDto(event.getCategory())); + dto.setInitiator(UserMapper.toUserShortDto(event.getInitiator())); + dto.setEventDate(event.getEventDate()); + dto.setPaid(event.getPaid()); + dto.setTitle(event.getTitle()); + return dto; + } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CategoryRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CategoryRepository.java index 0775f42..e8fe1ac 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CategoryRepository.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CategoryRepository.java @@ -3,5 +3,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import ru.practicum.evmsevice.model.Category; +import java.util.Optional; + public interface CategoryRepository extends JpaRepository { + Optional findCategoryById(int id); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminCategoryService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryService.java similarity index 74% rename from ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminCategoryService.java rename to ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryService.java index 5db2798..ca52503 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminCategoryService.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryService.java @@ -2,8 +2,9 @@ import ru.practicum.evmsevice.model.Category; -public interface AdminCategoryService { +public interface CategoryService { Category createCategory(Category category); Category updateCategory(Category category); void deleteCategory(Integer id); + Category getCategoryById(Integer id); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminCategoryServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryServiceImpl.java similarity index 78% rename from ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminCategoryServiceImpl.java rename to ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryServiceImpl.java index 3f2d472..deb848d 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminCategoryServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryServiceImpl.java @@ -10,7 +10,7 @@ @Service @Transactional @RequiredArgsConstructor -public class AdminCategoryServiceImpl implements AdminCategoryService { +public class CategoryServiceImpl implements CategoryService { private final CategoryRepository categoryRepository; @Override @@ -33,4 +33,11 @@ public void deleteCategory(Integer id) { .orElseThrow(() -> new NotFoundException("Не найдена категория id=" + id)); categoryRepository.deleteById(id); } + + @Override + public Category getCategoryById(Integer id) { + Category category = categoryRepository.findCategoryById(id) + .orElseThrow(() -> new NotFoundException("Не найдена категория id=" + id)); + return category; + } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java new file mode 100644 index 0000000..54a3bb4 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java @@ -0,0 +1,17 @@ +package ru.practicum.evmsevice.service; + +import ru.practicum.evmsevice.dto.EventFullDto; +import ru.practicum.evmsevice.dto.EventShortDto; +import ru.practicum.evmsevice.dto.NewEventDto; + +import java.util.List; + +public interface EventService { + EventFullDto createEvent(NewEventDto newEventDto, Integer userId); + + EventFullDto getEventById(Integer eventId, Integer userId); + + List getEventsByUserId(Integer userId); + + EventFullDto patchEvent(Integer eventId, EventShortDto eventShortDto, Integer userId); +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java new file mode 100644 index 0000000..8efc914 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java @@ -0,0 +1,52 @@ +package ru.practicum.evmsevice.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.evmsevice.dto.EventFullDto; +import ru.practicum.evmsevice.dto.EventShortDto; +import ru.practicum.evmsevice.dto.NewEventDto; +import ru.practicum.evmsevice.mapper.EventMapper; +import ru.practicum.evmsevice.model.Category; +import ru.practicum.evmsevice.model.Event; +import ru.practicum.evmsevice.model.User; +import ru.practicum.evmsevice.repository.EventRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class EventServiceImpl implements EventService{ + private final EventRepository eventRepository; + private final UserService userService; + private final CategoryService categoryService; + + @Override + public EventFullDto createEvent(NewEventDto newEventDto, Integer userId) { + User user = userService.getUserById(userId); + Category category = categoryService.getCategoryById(newEventDto.getCategory()); + Event event = EventMapper.toEvent(newEventDto); + event.setInitiator(user); + event.setCategory(category); + event.setPublishedOn(LocalDateTime.now()); + Event savedEvent = eventRepository.save(event); + return EventMapper.toFullDto(savedEvent); + } + + @Override + public EventFullDto getEventById(Integer eventId, Integer userId) { + return null; + } + + @Override + public List getEventsByUserId(Integer userId) { + return List.of(); + } + + @Override + public EventFullDto patchEvent(Integer eventId, EventShortDto eventShortDto, Integer userId) { + return null; + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminUserService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/UserService.java similarity index 86% rename from ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminUserService.java rename to ewm-service/src/main/java/ru/practicum/evmsevice/service/UserService.java index ac1b526..1f0d474 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminUserService.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/UserService.java @@ -4,7 +4,7 @@ import java.util.List; -public interface AdminUserService { +public interface UserService { List getUsers(); User getUserById(Integer id); diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminUserServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/UserServiceImpl.java similarity index 90% rename from ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminUserServiceImpl.java rename to ewm-service/src/main/java/ru/practicum/evmsevice/service/UserServiceImpl.java index e8e9cd6..bfc7e28 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/AdminUserServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/UserServiceImpl.java @@ -8,10 +8,10 @@ import java.util.List; @Service -public class AdminUserServiceImpl implements AdminUserService { +public class UserServiceImpl implements UserService { private final UserRepository userRepository; - public AdminUserServiceImpl(UserRepository userRepository) { + public UserServiceImpl(UserRepository userRepository) { this.userRepository = userRepository; } diff --git a/ewm-service/src/main/resources/application.properties b/ewm-service/src/main/resources/application.properties index 7bb57dd..5cc5bd2 100644 --- a/ewm-service/src/main/resources/application.properties +++ b/ewm-service/src/main/resources/application.properties @@ -3,14 +3,14 @@ spring.application.name=ewm-service spring.sql.init.mode=always spring.jackson.date-format=yyyy-MM-dd HH:mm:ss -spring.datasource.driverClassName=org.postgresql.Driver -spring.datasource.url=jdbc:postgresql://192.168.0.102:5434/ewmdb -spring.datasource.username=ewmdb -spring.datasource.password=ewmdb - -#spring.datasource.driverClassName=org.h2.Driver -#spring.datasource.url=jdbc:h2:mem:ewmdb +#spring.datasource.driverClassName=org.postgresql.Driver +#spring.datasource.url=jdbc:postgresql://192.168.0.102:5434/ewmdb #spring.datasource.username=ewmdb #spring.datasource.password=ewmdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.url=jdbc:h2:mem:ewmdb +spring.datasource.username=ewmdb +spring.datasource.password=ewmdb + statserver.url=http://localhost:9090 \ No newline at end of file From dce75ef3ed46821b005a4672d2cbf3c8d17a3a52 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Sun, 1 Jun 2025 21:57:42 +0700 Subject: [PATCH 10/29] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0?= =?UTF-8?q?=20=D1=81=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8=20=D0=BD=D0=B0=20=D1=83=D1=87=D0=B0=D1=81=D1=82=D0=B8?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../explore-with-me.postman_collection.json | 442 ++++++++++++++++++ Postman/objects.json | 57 +++ .../controller/AdminCategoryController.java | 57 --- .../evmsevice/controller/AdminController.java | 112 +++++ .../controller/AdminUserController.java | 66 --- .../evmsevice/controller/ErrorAdvisor.java | 28 +- .../evmsevice/controller/UserController.java | 68 ++- .../practicum/evmsevice/dto/EventFullDto.java | 3 +- .../practicum/evmsevice/dto/RequestDto.java | 19 + .../dto/UpdateEventAdminRequest.java | 13 + .../evmsevice/dto/UpdateEventUserRequest.java | 14 + .../evmsevice/enums/EventAdminAction.java | 6 + .../{model => enums}/EventState.java | 5 +- .../evmsevice/enums/EventUserAction.java | 6 + .../evmsevice/enums/RequestStatus.java | 19 + .../exception/ValidationException.java | 7 + .../evmsevice/mapper/EventMapper.java | 4 +- .../evmsevice/mapper/RequestMapper.java | 16 + .../ru/practicum/evmsevice/model/Event.java | 8 +- .../ru/practicum/evmsevice/model/Request.java | 31 ++ .../repository/CategoryRepository.java | 1 - .../evmsevice/repository/EventRepository.java | 3 + .../repository/RequestRepository.java | 13 + .../service/CategoryServiceImpl.java | 2 +- .../evmsevice/service/EventService.java | 13 +- .../evmsevice/service/EventServiceImpl.java | 186 +++++++- .../evmsevice/service/RequestService.java | 14 + .../evmsevice/service/RequestServiceImpl.java | 83 ++++ .../src/main/resources/application.properties | 14 +- ewm-service/src/main/resources/schema.sql | 9 +- 30 files changed, 1155 insertions(+), 164 deletions(-) create mode 100644 Postman/explore-with-me.postman_collection.json create mode 100644 Postman/objects.json delete mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminCategoryController.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java delete mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminUserController.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestDto.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/dto/UpdateEventAdminRequest.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/dto/UpdateEventUserRequest.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/enums/EventAdminAction.java rename ewm-service/src/main/java/ru/practicum/evmsevice/{model => enums}/EventState.java (85%) create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/enums/EventUserAction.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/enums/RequestStatus.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/exception/ValidationException.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/mapper/RequestMapper.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/model/Request.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java diff --git a/Postman/explore-with-me.postman_collection.json b/Postman/explore-with-me.postman_collection.json new file mode 100644 index 0000000..38b1bf1 --- /dev/null +++ b/Postman/explore-with-me.postman_collection.json @@ -0,0 +1,442 @@ +{ + "info": { + "_postman_id": "abbfaa88-2ad4-447b-a989-bb1f6dab9637", + "name": "explore-with-me", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "39468895" + }, + "item": [ + { + "name": "Stat", + "item": [ + { + "name": "Add Hit", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"app\": \"ewm-main-service\",\r\n \"uri\": \"/events/2\",\r\n \"ip\": \"192.163.0.1\",\r\n \"timestamp\": \"2022-09-06 11:00:23\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:9090/hit", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "9090", + "path": [ + "hit" + ] + } + }, + "response": [] + }, + { + "name": "get Stat", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"app\": \"ewm-main-service\",\r\n \"uri\": \"/events/1\",\r\n \"ip\": \"192.163.0.1\",\r\n \"timestamp\": \"2022-09-06 11:00:23\"\r\n}" + }, + "url": { + "raw": "http://localhost:9090/stats?uris=/events/1&uris=/events/2&unique=false", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "9090", + "path": [ + "stats" + ], + "query": [ + { + "key": "uris", + "value": "/events/1" + }, + { + "key": "uris", + "value": "/events/2" + }, + { + "key": "unique", + "value": "false" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Admin", + "item": [ + { + "name": "users", + "item": [ + { + "name": "Get users", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"app\": \"ewm-main-service\",\r\n \"uri\": \"/events/2\",\r\n \"ip\": \"192.163.0.1\",\r\n \"timestamp\": \"2022-09-06 11:00:23\"\r\n}" + }, + "url": { + "raw": "http://localhost:8080/admin/users", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Create user", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"email\": \"ivan.petrov@practicummail.ru\",\r\n \"name\": \"Иван Петров\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/admin/users", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Delete user", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "http://localhost:8080/admin/users/2", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "admin", + "users", + "2" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Categories", + "item": [ + { + "name": "Create category", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"email\": \"ivan.petrov@practicummail.ru\",\r\n \"name\": \"Иван Петров\"\r\n}" + }, + "url": { + "raw": "http://localhost:8080/admin/catgories", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "admin", + "catgories" + ] + } + }, + "response": [] + }, + { + "name": "Patch category", + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Соревнования\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/admin/categories/2", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "admin", + "categories", + "2" + ] + } + }, + "response": [] + }, + { + "name": "Delete category", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/admin/categories/3", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "admin", + "categories", + "3" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Events", + "item": [ + { + "name": "Patch event", + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://localhost:8080/admin/events/7", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "admin", + "events", + "7" + ] + } + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Users", + "item": [ + { + "name": "Events", + "item": [ + { + "name": "Create event", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"annotation\": \"Сплав на байдарках похож на полет.\",\r\n \"category\": 7,\r\n \"description\": \"Сплав на байдарках похож на полет. На спокойной воде — это парение. На бурной, порожистой — выполнение фигур высшего пилотажа. И то, и другое дарят чувство обновления, феерические эмоции, яркие впечатления.\",\r\n \"eventDate\": \"2025-05-31 23:55:05\",\r\n \"location\": {\r\n \"lat\": 55.754167,\r\n \"lon\": 37.62\r\n },\r\n \"paid\": true,\r\n \"participantLimit\": 10,\r\n \"requestModeration\": false,\r\n \"title\": \"Сплав на байдарках\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/users/1/events", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "1", + "events" + ] + } + }, + "response": [] + }, + { + "name": "Get event", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://localhost:8080/users/2/events/7", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "2", + "events", + "7" + ] + } + }, + "response": [] + }, + { + "name": "Get events", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8080/users/1/events", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "1", + "events" + ] + } + }, + "response": [] + }, + { + "name": "Update event", + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"eventDate\": \"2025-09-31 18:00:00\",\r\n \"title\": \"Концерт рок-группы 'Java Core'\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/users/1/events/2", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "1", + "events", + "2" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Requests", + "item": [ + { + "name": "Create request", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:8080/users/1/requests?eventId=8", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "1", + "requests" + ], + "query": [ + { + "key": "eventId", + "value": "8" + } + ] + } + }, + "response": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/Postman/objects.json b/Postman/objects.json new file mode 100644 index 0000000..a8f006d --- /dev/null +++ b/Postman/objects.json @@ -0,0 +1,57 @@ +{ + "users": [ + { + "id": 1, + "name": "Иван Петров", + "email": "ivan.petrov@practicum.mail.ru" + }, + { + "id": 2, + "name": "Николай Кузнецов", + "email": "nikolay.ruznetsov@practicum.mail.ru" + }, + { + "id": 3, + "name": "Владимир Петров", + "email": "vladimir.petrov@practicum.mail.ru" + } + ] +}, + +"events": [ + { + "id": 1, + "annotation": "Сплав на байдарках похож на полет.", + "category": { + "id": 2, + "name": "Соревнования" + }, + "confirmedRequest": null, + "eventDate": "2025-06-30T23:55:05", + "initiator": { + "id": 1, + "name": "Иван Петров" + }, + "paid": true, + "title": "Сплав на байдарках", + "views": null + }, + { + "id": 2, + "annotation": "За почти три десятилетия группа 'Java Core' закрепились на сцене как группа, объединяющая поколения.", + "category": { + "id": 1, + "name": "Концерты" + }, + "confirmedRequest": null, + "eventDate": "2025-09-30T18:00:00", + "initiator": { + "id": 1, + "name": "Иван Петров" + }, + "paid": true, + "title": "Концерт рок-группы 'Java Core'", + "views": null + } +] +} \ No newline at end of file diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminCategoryController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminCategoryController.java deleted file mode 100644 index a1076f9..0000000 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminCategoryController.java +++ /dev/null @@ -1,57 +0,0 @@ -package ru.practicum.evmsevice.controller; - -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; -import ru.practicum.evmsevice.client.StatsClient; -import ru.practicum.evmsevice.dto.CategoryDto; -import ru.practicum.evmsevice.dto.NewCategoryDto; -import ru.practicum.evmsevice.mapper.CategoryMapper; -import ru.practicum.evmsevice.model.Category; -import ru.practicum.evmsevice.service.CategoryService; - -@Slf4j -@RequiredArgsConstructor -@RestController -@RequestMapping("/admin/categories") -public class AdminCategoryController { - @Value("${spring.application.name}") - private String appName; - private final StatsClient statsClient; - private final CategoryService categoryService; - - @PostMapping - @ResponseStatus(HttpStatus.CREATED) - public CategoryDto createCategory(@Validated @RequestBody NewCategoryDto categoryDto, - HttpServletRequest request) { - log.info("Создаем категорию {}.", categoryDto.getName()); - statsClient.hitInfo(appName, request); - Category newCategory = categoryService.createCategory(CategoryMapper.toCategory(categoryDto)); - return CategoryMapper.toDto(newCategory); - } - - @PatchMapping("/{id}") - @ResponseStatus(HttpStatus.OK) - public CategoryDto updateCategory(@Validated @RequestBody NewCategoryDto categoryDto, - @PathVariable int id, - HttpServletRequest request) { - log.info("Обновляем категорию id={}.", id); - statsClient.hitInfo(appName, request); - Category category = CategoryMapper.toCategory(categoryDto); - category.setId(id); - Category updatedCategory = categoryService.updateCategory(category); - return CategoryMapper.toDto(updatedCategory); - } - - @DeleteMapping("/{id}") - @ResponseStatus(HttpStatus.OK) - public void deletecategory(@PathVariable int id, HttpServletRequest request) { - log.info("Удаляем категорию id={}.", id); - statsClient.hitInfo(appName, request); - categoryService.deleteCategory(id); - } -} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java new file mode 100644 index 0000000..1c84ba5 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java @@ -0,0 +1,112 @@ +package ru.practicum.evmsevice.controller; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.evmsevice.client.StatsClient; +import ru.practicum.evmsevice.dto.*; +import ru.practicum.evmsevice.mapper.CategoryMapper; +import ru.practicum.evmsevice.mapper.UserMapper; +import ru.practicum.evmsevice.model.Category; +import ru.practicum.evmsevice.model.Event; +import ru.practicum.evmsevice.model.User; +import ru.practicum.evmsevice.service.CategoryService; +import ru.practicum.evmsevice.service.EventService; +import ru.practicum.evmsevice.service.UserService; + +import java.util.List; + +/** + * Класс обработки запросов администратора + */ +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping("/admin") +public class AdminController { + @Value("${spring.application.name}") + private String appName; + private final StatsClient statsClient; + private final UserService userService; + private final CategoryService categoryService; + private final EventService eventService; + + @GetMapping("/users") + @ResponseStatus(HttpStatus.OK) + public List getUsers(HttpServletRequest request) { + log.info("{} запрашивает список пользователей.", request.getRemoteUser()); + statsClient.hitInfo(appName, request); + return userService.getUsers().stream() + .map(UserMapper::toUserDto) + .toList(); + } + + @GetMapping("/users/{id}") + @ResponseStatus(HttpStatus.OK) + public UserDto getUser(HttpServletRequest request, @PathVariable Integer id) { + log.info("Выполняем поиск пользователя id={}.", id); + statsClient.hitInfo(appName, request); + User user = userService.getUserById(id); + return UserMapper.toUserDto(user); + } + + @PostMapping("/users") + @ResponseStatus(HttpStatus.CREATED) + public UserDto createUser(@Validated @RequestBody UserDto userDto, HttpServletRequest request) { + log.info("Создаем нового пользователя {}", userDto.toString()); + statsClient.hitInfo(appName, request); + User savedUser = userService.addUser(UserMapper.toUser(userDto)); + return UserMapper.toUserDto(savedUser); + } + + @DeleteMapping("/users/{id}") + @ResponseStatus(HttpStatus.OK) + public void deleteUser(HttpServletRequest request, @PathVariable Integer id) { + log.info("Удаляем пользователя {}", id); + statsClient.hitInfo(appName, request); + userService.deleteUser(id); + } + + @PostMapping("/categories") + @ResponseStatus(HttpStatus.CREATED) + public CategoryDto createCategory(@Validated @RequestBody NewCategoryDto categoryDto, + HttpServletRequest request) { + log.info("Создаем категорию {}.", categoryDto.getName()); + statsClient.hitInfo(appName, request); + Category newCategory = categoryService.createCategory(CategoryMapper.toCategory(categoryDto)); + return CategoryMapper.toDto(newCategory); + } + + @PatchMapping("/categories/{id}") + @ResponseStatus(HttpStatus.OK) + public CategoryDto updateCategory(@Validated @RequestBody NewCategoryDto categoryDto, + @PathVariable int id, + HttpServletRequest request) { + log.info("Обновляем категорию id={}.", id); + statsClient.hitInfo(appName, request); + Category category = CategoryMapper.toCategory(categoryDto); + category.setId(id); + Category updatedCategory = categoryService.updateCategory(category); + return CategoryMapper.toDto(updatedCategory); + } + + @DeleteMapping("/categories/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deletecategory(@PathVariable int id, HttpServletRequest request) { + log.info("Администратор удаляет категорию id={}.", id); + statsClient.hitInfo(appName, request); + categoryService.deleteCategory(id); + } + + @PatchMapping("/events/{eventId}") + @ResponseStatus(HttpStatus.OK) + public EventFullDto updateEvent(@PathVariable Integer eventId, + @RequestBody UpdateEventAdminRequest eventDto) { + log.info("Администратор модерирует событие id={}.", eventId); + return eventService.adminUpdateEvent(eventId, eventDto); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminUserController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminUserController.java deleted file mode 100644 index 99f5686..0000000 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminUserController.java +++ /dev/null @@ -1,66 +0,0 @@ -package ru.practicum.evmsevice.controller; - -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; -import ru.practicum.evmsevice.client.StatsClient; -import ru.practicum.evmsevice.dto.UserDto; -import ru.practicum.evmsevice.mapper.UserMapper; -import ru.practicum.evmsevice.model.User; -import ru.practicum.evmsevice.service.UserService; - -import java.util.List; - -/** - * Класс административных запросов по объектам "User" - */ -@Slf4j -@RequiredArgsConstructor -@RestController -@RequestMapping("/admin/users") -public class AdminUserController { - @Value("${spring.application.name}") - private String appName; - private final StatsClient statsClient; - private final UserService userService; - - @GetMapping - @ResponseStatus(HttpStatus.OK) - public List getUsers(HttpServletRequest request) { - log.info("{} запрашивает список пользователей.", request.getRemoteUser()); - statsClient.hitInfo(appName, request); - return userService.getUsers().stream() - .map(UserMapper::toUserDto) - .toList(); - } - - @GetMapping("/{id}") - @ResponseStatus(HttpStatus.OK) - public UserDto getUser(HttpServletRequest request, @PathVariable Integer id) { - log.info("Выполняем поиск пользователя id={}.", id); - statsClient.hitInfo(appName, request); - User user = userService.getUserById(id); - return UserMapper.toUserDto(user); - } - - @PostMapping - @ResponseStatus(HttpStatus.CREATED) - public UserDto createUser(@Validated @RequestBody UserDto userDto, HttpServletRequest request) { - log.info("Создаем нового пользователя {}", userDto.toString()); - statsClient.hitInfo(appName, request); - User savedUser = userService.addUser(UserMapper.toUser(userDto)); - return UserMapper.toUserDto(savedUser); - } - - @DeleteMapping("/{id}") - @ResponseStatus(HttpStatus.OK) - public void deleteUser(HttpServletRequest request, @PathVariable Integer id) { - log.info("Удаляем пользователя {}", id); - statsClient.hitInfo(appName, request); - userService.deleteUser(id); - } -} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java index 293f934..c443421 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java @@ -8,9 +8,11 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import ru.practicum.evmsevice.dto.ApiError; import ru.practicum.evmsevice.exception.InternalServerException; import ru.practicum.evmsevice.exception.NotFoundException; +import ru.practicum.evmsevice.exception.ValidationException; import java.time.LocalDateTime; import java.util.List; @@ -35,6 +37,18 @@ public ApiError notFoundObject(NotFoundException exception) { return apiError; } + @ExceptionHandler(ValidationException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public ApiError onValidationException(ValidationException exception) { + log.error("409 {}.", exception.getMessage()); + ApiError apiError = new ApiError(); + apiError.setStatus(HttpStatus.FORBIDDEN); + apiError.setReason("Запрос содержит недопустимые данные."); + apiError.setMessage(exception.getMessage()); + apiError.setTimestamp(LocalDateTime.now()); + return apiError; + } + @ExceptionHandler(InternalServerException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ApiError onInternalException(final InternalServerException e) { @@ -47,7 +61,6 @@ public ApiError onInternalException(final InternalServerException e) { return apiError; } - @ExceptionHandler(DataIntegrityViolationException.class) @ResponseStatus(HttpStatus.CONFLICT) public ApiError onDataIntegrityViolationException(final DataIntegrityViolationException e) { @@ -71,7 +84,6 @@ public ApiError onMethodArgumentNotValidException(MethodArgumentNotValidExceptio error.getRejectedValue() )) .collect(Collectors.toList()); - ApiError apiError = new ApiError(); apiError.setStatus(HttpStatus.BAD_REQUEST); apiError.setReason("Запрос сформирован некорректно."); @@ -92,13 +104,23 @@ public ApiError onConstraintValidationException(ConstraintViolationException e) violation.getInvalidValue() )) .collect(Collectors.toList()); - ApiError apiError = new ApiError(); apiError.setStatus(HttpStatus.BAD_REQUEST); apiError.setReason("Запрос сформирован некорректно."); apiError.setMessage(violations.stream().collect(Collectors.joining())); apiError.setTimestamp(LocalDateTime.now()); + return apiError; + } + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiError onMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) { + log.error("400 {}.", e.getMessage()); + ApiError apiError = new ApiError(); + apiError.setStatus(HttpStatus.BAD_REQUEST); + apiError.setReason("Incorrectly made request."); + apiError.setMessage(e.getMessage()); + apiError.setTimestamp(LocalDateTime.now()); return apiError; } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java index f2fe435..f45fb47 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java @@ -5,23 +5,85 @@ import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import ru.practicum.evmsevice.dto.EventFullDto; -import ru.practicum.evmsevice.dto.NewEventDto; +import ru.practicum.evmsevice.dto.*; +import ru.practicum.evmsevice.mapper.RequestMapper; import ru.practicum.evmsevice.model.Event; +import ru.practicum.evmsevice.model.Request; import ru.practicum.evmsevice.service.EventService; +import ru.practicum.evmsevice.service.RequestService; import ru.practicum.evmsevice.service.UserService; +import java.util.List; + @Slf4j @RequiredArgsConstructor @RestController @RequestMapping("/users")public class UserController { private final EventService eventService; + private final RequestService requestService; @PostMapping("/{id}/events") @ResponseStatus(HttpStatus.CREATED) public EventFullDto createEvent(@PathVariable int id, @Validated @RequestBody NewEventDto eventDto) { - log.info("Creating new event {}", eventDto); + log.info("Пользователь id={} cоздает новое событие: {}", id, eventDto.getTitle()); return eventService.createEvent(eventDto, id); } + + @GetMapping("/{userId}/events/{eventId}") + @ResponseStatus(HttpStatus.OK) + public EventFullDto getEvent(@PathVariable int userId, + @PathVariable int eventId) { + + log.info("Пользователь id={} запрашивает информацию о событии id={}. ", + userId, eventId); + return eventService.getEventById(eventId, userId); + } + + @GetMapping("/{userId}/events") + @ResponseStatus(HttpStatus.OK) + public List getEvents(@PathVariable int userId, + @RequestParam(name = "from", defaultValue = "0") Integer from, + @RequestParam(name = "size", defaultValue = "10") Integer size) { + log.info("Пользователь id={} запрашивает информацию об инциированных событиях.", userId); + return eventService.getEventsByUserId(userId, from, size); + } + + @PatchMapping("/{userId}/events/{eventId}") + @ResponseStatus(HttpStatus.OK) + public EventFullDto updateEvent(@PathVariable Integer userId, + @PathVariable Integer eventId, + @RequestBody UpdateEventUserRequest eventDto) { + log.info("Пользователь id={} изменяет информацию об инциированном событии id={}.", + userId, eventId); + return eventService.patchEvent(eventId, eventDto, userId); + } + + @PostMapping("/{userId}/requests") + @ResponseStatus(HttpStatus.CREATED) + public RequestDto createRequet(@PathVariable Integer userId, + @RequestParam(name = "eventId", required = true) Integer eventId) { + log.info("Пользователь id={} создает запрос на участие в событии id={}.", + userId, eventId); + Request request = requestService.createRequest(userId, eventId); + return RequestMapper.toRequestDto(request); + } + + @GetMapping("/{userId}/requests") + @ResponseStatus(HttpStatus.OK) + public List findRequestsByUserId(@PathVariable Integer userId) { + log.info("Пользователь id={} выполняет поиск собственных запросов.", userId); + return requestService.getRequestsByUserId(userId) + .stream() + .map(RequestMapper::toRequestDto) + .toList(); + } + + @DeleteMapping("/{userId}/requests/{requestId}/cancel") + @ResponseStatus(HttpStatus.OK) + public RequestDto deleteRequestById( @PathVariable Integer userId, + @PathVariable Integer requestId) { + log.info("Пользователь id={} удаляет запрос id={}.", userId, requestId); + return RequestMapper.toRequestDto(requestService.deleteRequest(userId, requestId)); + } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventFullDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventFullDto.java index e3f89de..b83d48f 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventFullDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventFullDto.java @@ -5,6 +5,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import ru.practicum.evmsevice.enums.EventState; import ru.practicum.evmsevice.model.Location; import java.time.LocalDateTime; @@ -29,7 +30,7 @@ public class EventFullDto { @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime publishedOn; private Boolean requestModeration; - private String state; + private EventState state; @Size(min = 3, max = 120, message = "длина заголовка 3 - 120 символов.") private String title; private Integer views; diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestDto.java new file mode 100644 index 0000000..921e11a --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestDto.java @@ -0,0 +1,19 @@ +package ru.practicum.evmsevice.dto; + +import lombok.*; +import ru.practicum.evmsevice.enums.RequestStatus; + +import java.time.LocalDateTime; + +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class RequestDto { + private Integer id; + private LocalDateTime created; + private Integer event; + private Integer requester; + private RequestStatus status; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UpdateEventAdminRequest.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UpdateEventAdminRequest.java new file mode 100644 index 0000000..a8d1244 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UpdateEventAdminRequest.java @@ -0,0 +1,13 @@ +package ru.practicum.evmsevice.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.evmsevice.enums.EventAdminAction; + +@Setter +@Getter +@NoArgsConstructor +public class UpdateEventAdminRequest extends NewEventDto{ + private EventAdminAction stateAction; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UpdateEventUserRequest.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UpdateEventUserRequest.java new file mode 100644 index 0000000..f841f22 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UpdateEventUserRequest.java @@ -0,0 +1,14 @@ +package ru.practicum.evmsevice.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.evmsevice.enums.EventState; +import ru.practicum.evmsevice.enums.EventUserAction; + +@Setter +@Getter +@NoArgsConstructor +public class UpdateEventUserRequest extends NewEventDto{ + private EventUserAction stateAction; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/enums/EventAdminAction.java b/ewm-service/src/main/java/ru/practicum/evmsevice/enums/EventAdminAction.java new file mode 100644 index 0000000..da25f78 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/enums/EventAdminAction.java @@ -0,0 +1,6 @@ +package ru.practicum.evmsevice.enums; + +public enum EventAdminAction { + PUBLISH_EVENT, + REJECT_EVENT; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/EventState.java b/ewm-service/src/main/java/ru/practicum/evmsevice/enums/EventState.java similarity index 85% rename from ewm-service/src/main/java/ru/practicum/evmsevice/model/EventState.java rename to ewm-service/src/main/java/ru/practicum/evmsevice/enums/EventState.java index 0d866d0..51db4ed 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/model/EventState.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/enums/EventState.java @@ -1,11 +1,12 @@ -package ru.practicum.evmsevice.model; +package ru.practicum.evmsevice.enums; import java.util.Optional; public enum EventState { PENDING, PUBLISHED, - CANCELED; + CANCELED, + REJECTED; public static Optional from(String state) { for (EventState eventState : EventState.values()) { diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/enums/EventUserAction.java b/ewm-service/src/main/java/ru/practicum/evmsevice/enums/EventUserAction.java new file mode 100644 index 0000000..3bae47a --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/enums/EventUserAction.java @@ -0,0 +1,6 @@ +package ru.practicum.evmsevice.enums; + +public enum EventUserAction { + SEND_TO_REVIEW, + CANCEL_REVIEW; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/enums/RequestStatus.java b/ewm-service/src/main/java/ru/practicum/evmsevice/enums/RequestStatus.java new file mode 100644 index 0000000..0137965 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/enums/RequestStatus.java @@ -0,0 +1,19 @@ +package ru.practicum.evmsevice.enums; + +import java.util.Optional; + +public enum RequestStatus { + PENDING, + APPROVED, + REJECTED, + CANCELED; + + public static Optional from(String state) { + for (RequestStatus status : RequestStatus.values()) { + if (status.name().equalsIgnoreCase(state)) { + return Optional.of(status); + } + } + return Optional.empty(); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/exception/ValidationException.java b/ewm-service/src/main/java/ru/practicum/evmsevice/exception/ValidationException.java new file mode 100644 index 0000000..2a98665 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/exception/ValidationException.java @@ -0,0 +1,7 @@ +package ru.practicum.evmsevice.exception; + +public class ValidationException extends RuntimeException { + public ValidationException(String message) { + super(message); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java index 61ec177..a59b5bc 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java @@ -4,7 +4,7 @@ import ru.practicum.evmsevice.dto.EventShortDto; import ru.practicum.evmsevice.dto.NewEventDto; import ru.practicum.evmsevice.model.Event; -import ru.practicum.evmsevice.model.EventState; +import ru.practicum.evmsevice.enums.EventState; import ru.practicum.evmsevice.model.Location; import java.time.LocalDateTime; @@ -23,7 +23,7 @@ public static Event toEvent(final NewEventDto newDto) { event.setPaid(newDto.getPaid()); event.setParticipantLimit(newDto.getParticipantLimit()); event.setRequestModeration(newDto.getRequestModeration()); - event.setState(EventState.PENDING.name()); + event.setState(EventState.PENDING); event.setTitle(newDto.getTitle()); return event; } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/RequestMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/RequestMapper.java new file mode 100644 index 0000000..b523d60 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/RequestMapper.java @@ -0,0 +1,16 @@ +package ru.practicum.evmsevice.mapper; + +import ru.practicum.evmsevice.dto.RequestDto; +import ru.practicum.evmsevice.model.Request; + +public class RequestMapper { + private RequestMapper() {} + public static RequestDto toRequestDto(Request request) { + RequestDto requestDto = new RequestDto(); + requestDto.setId(request.getId()); + requestDto.setCreated(request.getCreated()); + requestDto.setEvent(request.getEvent().getId()); + requestDto.setRequester(request.getRequester().getId()); + return requestDto; + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java index b03df9d..bf549f9 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java @@ -4,13 +4,14 @@ import lombok.Getter; import lombok.Setter; import lombok.NoArgsConstructor; +import ru.practicum.evmsevice.enums.EventState; import java.time.LocalDateTime; @Entity @Setter @Getter -@Table(name = "eventss", schema = "public") +@Table(name = "events", schema = "public") @NoArgsConstructor public class Event { @Id @@ -30,7 +31,7 @@ public class Event { @Column(name = "eventdate", nullable = false) private LocalDateTime eventDate; @ManyToOne - @JoinColumn(name = "user_id") + @JoinColumn(name = "initiator_id") private User initiator; @Column(name = "lat") private Float lat; @@ -45,7 +46,8 @@ public class Event { @Column(name = "requestmoderation") private Boolean requestModeration; @Column(name = "state") - private String state; + @Enumerated(EnumType.STRING) + private EventState state; @Column(name = "title") private String title; } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Request.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Request.java new file mode 100644 index 0000000..6e03ecb --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Request.java @@ -0,0 +1,31 @@ +package ru.practicum.evmsevice.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.evmsevice.enums.RequestStatus; + +import java.time.LocalDateTime; + +@Entity +@Setter +@Getter +@Table(name = "requests", schema = "public") +@NoArgsConstructor +public class Request { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + @ManyToOne + @JoinColumn(name = "requester_id") + private User requester; + @ManyToOne + @JoinColumn(name = "event_id") + private Event event; + @Column(name = "status") + @Enumerated(EnumType.STRING) + private RequestStatus status; + @Column(name = "created", nullable = false) + private LocalDateTime created; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CategoryRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CategoryRepository.java index e8fe1ac..61f4d91 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CategoryRepository.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CategoryRepository.java @@ -6,5 +6,4 @@ import java.util.Optional; public interface CategoryRepository extends JpaRepository { - Optional findCategoryById(int id); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java index 15f3ca3..b0386e8 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java @@ -3,5 +3,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import ru.practicum.evmsevice.model.Event; +import java.util.List; + public interface EventRepository extends JpaRepository { + List findEventsByInitiator_Id(int id); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java new file mode 100644 index 0000000..8bb7537 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java @@ -0,0 +1,13 @@ +package ru.practicum.evmsevice.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.evmsevice.enums.RequestStatus; +import ru.practicum.evmsevice.model.Request; + +import java.util.List; + +public interface RequestRepository extends JpaRepository { + List findAllByRequester_Id(int userId); + + // Integer countByEvent_IdAAndStatusEquals(int eventId, RequestStatus status); +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryServiceImpl.java index deb848d..5befe38 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryServiceImpl.java @@ -36,7 +36,7 @@ public void deleteCategory(Integer id) { @Override public Category getCategoryById(Integer id) { - Category category = categoryRepository.findCategoryById(id) + Category category = categoryRepository.findById(id) .orElseThrow(() -> new NotFoundException("Не найдена категория id=" + id)); return category; } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java index 54a3bb4..6f49208 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java @@ -1,8 +1,7 @@ package ru.practicum.evmsevice.service; -import ru.practicum.evmsevice.dto.EventFullDto; -import ru.practicum.evmsevice.dto.EventShortDto; -import ru.practicum.evmsevice.dto.NewEventDto; +import ru.practicum.evmsevice.dto.*; +import ru.practicum.evmsevice.model.Event; import java.util.List; @@ -11,7 +10,11 @@ public interface EventService { EventFullDto getEventById(Integer eventId, Integer userId); - List getEventsByUserId(Integer userId); + List getEventsByUserId(Integer userId, Integer from, Integer size); - EventFullDto patchEvent(Integer eventId, EventShortDto eventShortDto, Integer userId); + EventFullDto patchEvent(Integer eventId, UpdateEventUserRequest eventDto, Integer userId); + + EventFullDto adminUpdateEvent(Integer eventId, UpdateEventAdminRequest eventDto); + + Event findEventById(Integer eventId); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java index 8efc914..1f638b3 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java @@ -3,9 +3,12 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import ru.practicum.evmsevice.dto.EventFullDto; -import ru.practicum.evmsevice.dto.EventShortDto; -import ru.practicum.evmsevice.dto.NewEventDto; +import ru.practicum.evmsevice.dto.*; +import ru.practicum.evmsevice.enums.EventAdminAction; +import ru.practicum.evmsevice.enums.EventState; +import ru.practicum.evmsevice.enums.EventUserAction; +import ru.practicum.evmsevice.exception.NotFoundException; +import ru.practicum.evmsevice.exception.ValidationException; import ru.practicum.evmsevice.mapper.EventMapper; import ru.practicum.evmsevice.model.Category; import ru.practicum.evmsevice.model.Event; @@ -13,40 +16,205 @@ import ru.practicum.evmsevice.repository.EventRepository; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; @Service @Transactional @RequiredArgsConstructor public class EventServiceImpl implements EventService{ + private static final DateTimeFormatter DATA_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final Integer HOURS_EVENT_DELAY = 2; + private final EventRepository eventRepository; private final UserService userService; private final CategoryService categoryService; + /** + * Создание нового события + * @param newEventDto - новое событие + * @param userId - иденитификатор пользователя инициатора + * @return - сохраненный объект информации о событии + */ @Override public EventFullDto createEvent(NewEventDto newEventDto, Integer userId) { + if (newEventDto.getEventDate().isBefore(LocalDateTime.now().plusHours(HOURS_EVENT_DELAY))) { + throw new ValidationException( + "Field: eventDate. Error: не может быть раньше, чем через " + + HOURS_EVENT_DELAY + " часа от текущего момента. Value: " + + newEventDto.getEventDate().format(DATA_TIME_FORMATTER) + ); + } User user = userService.getUserById(userId); Category category = categoryService.getCategoryById(newEventDto.getCategory()); Event event = EventMapper.toEvent(newEventDto); event.setInitiator(user); event.setCategory(category); - event.setPublishedOn(LocalDateTime.now()); Event savedEvent = eventRepository.save(event); return EventMapper.toFullDto(savedEvent); } @Override public EventFullDto getEventById(Integer eventId, Integer userId) { - return null; + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> + new NotFoundException("Не найдено событие id=" + eventId)); + if (!event.getInitiator().getId().equals(userId)) { + throw new ValidationException("Пользователь id=" + userId + + " не является инициатором события id=" + eventId); + } + return EventMapper.toFullDto(event); + } + + @Override + public List getEventsByUserId(Integer userId, Integer from, Integer size) { + List events = eventRepository.findEventsByInitiator_Id(userId); + return events.stream() + .skip(from) + .limit(size) + .map(EventMapper::toShortDto).toList(); } @Override - public List getEventsByUserId(Integer userId) { - return List.of(); + public EventFullDto patchEvent(Integer eventId, UpdateEventUserRequest eventDto, Integer userId) { + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> + new NotFoundException("Не найдено событие id=" + eventId)); + if (!event.getInitiator().getId().equals(userId)) { + throw new ValidationException("Пользователь id=" + userId + + " не является инициатором события id=" + eventId); + } + if (event.getEventDate().isBefore(LocalDateTime.now().plusHours(HOURS_EVENT_DELAY))) { + throw new ValidationException( + "Field: eventDate. Error: не может быть раньше, чем через " + + HOURS_EVENT_DELAY + " часа от текущего момента. Value: " + + event.getEventDate().format(DATA_TIME_FORMATTER) + ); + } + if (eventDto.getAnnotation() != null) { + event.setAnnotation(eventDto.getAnnotation()); + } + if (eventDto.getCategory() != null) { + event.setCategory(categoryService.getCategoryById(eventDto.getCategory())); + } + if (eventDto.getDescription() != null) { + event.setDescription(eventDto.getDescription()); + } + if (eventDto.getEventDate() != null) { + if (eventDto.getEventDate().isBefore(LocalDateTime.now().plusHours(HOURS_EVENT_DELAY))) { + throw new ValidationException( + "Field: eventDate. Error: новое значение не может быть раньше, чем через " + + HOURS_EVENT_DELAY + " часа от текущего момента. Value: " + + eventDto.getEventDate().format(DATA_TIME_FORMATTER) + ); + } + event.setEventDate(eventDto.getEventDate()); + } + if (eventDto.getLocation() != null) { + event.setLat(eventDto.getLocation().getLat()); + event.setLon(eventDto.getLocation().getLon()); + } + if (eventDto.getPaid() != null) { + event.setPaid(eventDto.getPaid()); + } + if (eventDto.getParticipantLimit() != null) { + event.setParticipantLimit(eventDto.getParticipantLimit()); + } + if (eventDto.getRequestModeration() != null) { + event.setRequestModeration(eventDto.getRequestModeration()); + } + if (eventDto.getStateAction() != null) { + if (eventDto.getStateAction().equals(EventUserAction.CANCEL_REVIEW)) { + event.setState(EventState.CANCELED); + } else if (eventDto.getStateAction().equals(EventUserAction.SEND_TO_REVIEW)) { + event.setState(EventState.PENDING); + } + } + if (eventDto.getTitle() != null) { + event.setTitle(eventDto.getTitle()); + } + Event savedEvent = eventRepository.save(event); + return EventMapper.toFullDto(savedEvent); + } + + @Override + public EventFullDto adminUpdateEvent(Integer eventId, UpdateEventAdminRequest eventDto) { + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> + new NotFoundException("Не найдено событие id=" + eventId)); + if (event.getEventDate().isBefore(LocalDateTime.now().plusHours(HOURS_EVENT_DELAY))) { + throw new ValidationException( + "Field: eventDate. Error: не может быть раньше, чем через " + + HOURS_EVENT_DELAY + " часа от текущего момента. Value: " + + event.getEventDate().format(DATA_TIME_FORMATTER) + ); + } + if (eventDto.getAnnotation() != null) { + event.setAnnotation(eventDto.getAnnotation()); + } + if (eventDto.getCategory() != null) { + event.setCategory(categoryService.getCategoryById(eventDto.getCategory())); + } + if (eventDto.getDescription() != null) { + event.setDescription(eventDto.getDescription()); + } + if (eventDto.getEventDate() != null) { + if (eventDto.getEventDate().isBefore(LocalDateTime.now().plusHours(HOURS_EVENT_DELAY))) { + throw new ValidationException( + "Field: eventDate. Error: новое значение не может быть раньше, чем через " + + HOURS_EVENT_DELAY + " часа от текущего момента. Value: " + + eventDto.getEventDate().format(DATA_TIME_FORMATTER) + ); + } + event.setEventDate(eventDto.getEventDate()); + } + if (eventDto.getLocation() != null) { + event.setLat(eventDto.getLocation().getLat()); + event.setLon(eventDto.getLocation().getLon()); + } + if (eventDto.getPaid() != null) { + event.setPaid(eventDto.getPaid()); + } + if (eventDto.getParticipantLimit() != null) { + event.setParticipantLimit(eventDto.getParticipantLimit()); + } + if (eventDto.getRequestModeration() != null) { + event.setRequestModeration(eventDto.getRequestModeration()); + } + if (eventDto.getStateAction() != null) { + if (eventDto.getStateAction().equals(EventAdminAction.PUBLISH_EVENT)) { + if (!event.getState().equals(EventState.PENDING)) { + throw new ValidationException( + "Field: stateAction. Error: " + + "Событие id=" + eventId + " должно быть в состоянии ожидания публикации." + + " Value: " + eventDto.getStateAction() + ); + } + event.setState(EventState.PUBLISHED); + event.setPublishedOn(LocalDateTime.now()); + } else if (eventDto.getStateAction().equals(EventAdminAction.REJECT_EVENT)) { + if(event.getState().equals(EventState.PUBLISHED)) { + throw new ValidationException( + "Field: stateAction. Error: " + + "Нельзя удалить опубликованное событие id=" + eventId + + " Value: " + eventDto.getStateAction() + ); + } + event.setState(EventState.REJECTED); + } + } + if (eventDto.getTitle() != null) { + event.setTitle(eventDto.getTitle()); + } + Event savedEvent = eventRepository.save(event); + return EventMapper.toFullDto(savedEvent); } @Override - public EventFullDto patchEvent(Integer eventId, EventShortDto eventShortDto, Integer userId) { - return null; + public Event findEventById(Integer eventId) { + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> + new NotFoundException("Не найдено событие id=" + eventId)); + return event; } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java new file mode 100644 index 0000000..3dafb43 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java @@ -0,0 +1,14 @@ +package ru.practicum.evmsevice.service; + +import ru.practicum.evmsevice.model.Event; +import ru.practicum.evmsevice.model.Request; + +import java.util.List; + +public interface RequestService { + Request createRequest(Integer userId, Integer eventId ); + + List getRequestsByUserId(Integer userId); + + Request deleteRequest(Integer userId, Integer requestId); +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java new file mode 100644 index 0000000..d4eee71 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java @@ -0,0 +1,83 @@ +package ru.practicum.evmsevice.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.evmsevice.enums.EventState; +import ru.practicum.evmsevice.enums.RequestStatus; +import ru.practicum.evmsevice.exception.NotFoundException; +import ru.practicum.evmsevice.exception.ValidationException; +import ru.practicum.evmsevice.model.*; +import ru.practicum.evmsevice.repository.RequestRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class RequestServiceImpl implements RequestService { + private final RequestRepository requestRepository; + private final EventService eventService; + private final UserService userService; + + @Override + public Request createRequest(Integer userId, Integer eventId) { + Event event = eventService.findEventById(eventId); + if (event.getInitiator().getId().equals(userId)) { + throw new ValidationException( + "Field: event.initiator_id. Error: " + + "Инициатор события не может добавить запрос на участие в своём событии. " + + "Value: " + event.getInitiator().getId() + ); + } + if (!event.getState().equals(EventState.PUBLISHED)) { + throw new ValidationException( + "Field: event.state. Error: " + + "Нельзя участвовать в неопубликованном событии. " + + "Value: " + event.getState() + ); + } + if (event.getConfirmedRequests().equals(event.getParticipantLimit())) { + throw new ValidationException( + "Field: event.state. Error: " + + "У события достигнут лимит запросов на участие. " + + "Value: " + event.getConfirmedRequests() + ); + } + Request request = new Request(); + User user = userService.getUserById(userId); + request.setRequester(user); + request.setEvent(event); + if (event.getRequestModeration()) { + request.setStatus(RequestStatus.PENDING); + } else { + request.setStatus(RequestStatus.APPROVED); + } + request.setCreated(LocalDateTime.now()); + Request savedRequest = requestRepository.save(request); + return savedRequest; + } + + @Override + public List getRequestsByUserId(Integer userId) { + List requests = requestRepository.findAllByRequester_Id(userId); + return requests; + } + + @Override + public Request deleteRequest(Integer userId, Integer requestId) { + Request request = requestRepository.findById(requestId) + .orElseThrow(() -> + new NotFoundException("Не найден запрос id=" + requestId)); + if (!request.getRequester().getId().equals(userId)) { + throw new ValidationException( + "Field: request.requestor.id. Error: " + + "Нельзя удалить чужой запрос. " + + "Value: " + request.getRequester().getId() + ); + } + requestRepository.delete(request); + return request; + } +} diff --git a/ewm-service/src/main/resources/application.properties b/ewm-service/src/main/resources/application.properties index 5cc5bd2..7bb57dd 100644 --- a/ewm-service/src/main/resources/application.properties +++ b/ewm-service/src/main/resources/application.properties @@ -3,14 +3,14 @@ spring.application.name=ewm-service spring.sql.init.mode=always spring.jackson.date-format=yyyy-MM-dd HH:mm:ss -#spring.datasource.driverClassName=org.postgresql.Driver -#spring.datasource.url=jdbc:postgresql://192.168.0.102:5434/ewmdb -#spring.datasource.username=ewmdb -#spring.datasource.password=ewmdb - -spring.datasource.driverClassName=org.h2.Driver -spring.datasource.url=jdbc:h2:mem:ewmdb +spring.datasource.driverClassName=org.postgresql.Driver +spring.datasource.url=jdbc:postgresql://192.168.0.102:5434/ewmdb spring.datasource.username=ewmdb spring.datasource.password=ewmdb +#spring.datasource.driverClassName=org.h2.Driver +#spring.datasource.url=jdbc:h2:mem:ewmdb +#spring.datasource.username=ewmdb +#spring.datasource.password=ewmdb + statserver.url=http://localhost:9090 \ No newline at end of file diff --git a/ewm-service/src/main/resources/schema.sql b/ewm-service/src/main/resources/schema.sql index 9f63d24..b0d95bd 100644 --- a/ewm-service/src/main/resources/schema.sql +++ b/ewm-service/src/main/resources/schema.sql @@ -15,13 +15,13 @@ CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS events ( id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, - annotation VARCHAR(256), + annotation VARCHAR(2000), category_id INTEGER, confirmedRequests INTEGER, createdOn TIMESTAMP WITHOUT TIME ZONE, - description VARCHAR(256), + description VARCHAR(7000), eventDate TIMESTAMP WITHOUT TIME ZONE, - user_id INTEGER NOT NULL, + initiator_id INTEGER NOT NULL, lat FLOAT, lon FLOAT, paid BOOLEAN, @@ -43,7 +43,8 @@ CREATE TABLE IF NOT EXISTS requests ( created TIMESTAMP WITHOUT TIME ZONE, CONSTRAINT pk_request PRIMARY KEY (id), CONSTRAINT fk_requests_to_users FOREIGN KEY (requester_id) REFERENCES users (id), - CONSTRAINT fk_requests_to_events FOREIGN KEY (event_id) REFERENCES events (id) + CONSTRAINT fk_requests_to_events FOREIGN KEY (event_id) REFERENCES events (id), + CONSTRAINT unique_requester_event UNIQUE (requester_id, event_id) ); CREATE TABLE IF NOT EXISTS compilations ( From 3d5569529cdfebe8511ab3a701e7aca6e350da39 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Mon, 2 Jun 2025 22:06:59 +0700 Subject: [PATCH 11/29] =?UTF-8?q?fix:=20=D0=B4=D0=BE=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D0=B0=D0=BD=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=20=D1=81=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=B0?= =?UTF-8?q?=D0=BC=D0=B8=20=D0=BD=D0=B0=20=D1=83=D1=87=D0=B0=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../explore-with-me.postman_collection.json | 98 +++++++++++++++++++ Postman/objects.json | 3 +- .../evmsevice/controller/UserController.java | 12 +++ .../practicum/evmsevice/dto/RequestDto.java | 2 + .../evmsevice/mapper/EventMapper.java | 12 ++- .../evmsevice/mapper/RequestMapper.java | 1 + .../repository/RequestRepository.java | 2 + .../evmsevice/service/RequestService.java | 3 + .../evmsevice/service/RequestServiceImpl.java | 40 +++++--- 9 files changed, 156 insertions(+), 17 deletions(-) diff --git a/Postman/explore-with-me.postman_collection.json b/Postman/explore-with-me.postman_collection.json index 38b1bf1..903046e 100644 --- a/Postman/explore-with-me.postman_collection.json +++ b/Postman/explore-with-me.postman_collection.json @@ -401,6 +401,60 @@ } }, "response": [] + }, + { + "name": "Get request by event", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8080/users/3/events2/reqests", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "3", + "events2", + "reqests" + ] + } + }, + "response": [] + }, + { + "name": "Patch requests", + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"requestIds\": [\r\n 1,\r\n 2,\r\n 3\r\n ],\r\n \"status\": \"CONFIRMED\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/users/1/events/2/requests", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "1", + "events", + "2", + "requests" + ] + } + }, + "response": [] } ] }, @@ -433,6 +487,50 @@ } }, "response": [] + }, + { + "name": "Get requests", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8080/users/2/requests", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "2", + "requests" + ] + } + }, + "response": [] + }, + { + "name": "Delete request", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "http://localhost:8080/users/3/requests/4/cancel", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "3", + "requests", + "4", + "cancel" + ] + } + }, + "response": [] } ] } diff --git a/Postman/objects.json b/Postman/objects.json index a8f006d..855d29b 100644 --- a/Postman/objects.json +++ b/Postman/objects.json @@ -15,8 +15,7 @@ "name": "Владимир Петров", "email": "vladimir.petrov@practicum.mail.ru" } - ] -}, + ], "events": [ { diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java index f45fb47..f04c521 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java @@ -59,6 +59,18 @@ public EventFullDto updateEvent(@PathVariable Integer userId, return eventService.patchEvent(eventId, eventDto, userId); } + @GetMapping("/{userId}/events/{eventId}/requests") + @ResponseStatus(HttpStatus.OK) + public List findRequestsByEventId(@PathVariable int userId, + @PathVariable int eventId) { + log.info("Пользователь id={} выполняет поиск запросов на участие в событии id={}.", + userId, eventId); + return requestService.getRequestsByEventId(userId, eventId) + .stream() + .map(RequestMapper::toRequestDto) + .toList(); + } + @PostMapping("/{userId}/requests") @ResponseStatus(HttpStatus.CREATED) public RequestDto createRequet(@PathVariable Integer userId, diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestDto.java index 921e11a..606f4f4 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestDto.java @@ -1,5 +1,6 @@ package ru.practicum.evmsevice.dto; +import com.fasterxml.jackson.annotation.JsonFormat; import lombok.*; import ru.practicum.evmsevice.enums.RequestStatus; @@ -12,6 +13,7 @@ @ToString public class RequestDto { private Integer id; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime created; private Integer event; private Integer requester; diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java index a59b5bc..ec53933 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java @@ -20,9 +20,17 @@ public static Event toEvent(final NewEventDto newDto) { event.setCreatedOn(LocalDateTime.now()); event.setLat(newDto.getLocation().getLat()); event.setLon(newDto.getLocation().getLon()); - event.setPaid(newDto.getPaid()); + if(newDto.getPaid()) { + event.setPaid(newDto.getPaid()); + } else { + event.setPaid(false); + } event.setParticipantLimit(newDto.getParticipantLimit()); - event.setRequestModeration(newDto.getRequestModeration()); + if (newDto.getRequestModeration() != null) { + event.setRequestModeration(newDto.getRequestModeration()); + } else { + event.setRequestModeration(false); + } event.setState(EventState.PENDING); event.setTitle(newDto.getTitle()); return event; diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/RequestMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/RequestMapper.java index b523d60..a6ac9ff 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/RequestMapper.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/RequestMapper.java @@ -11,6 +11,7 @@ public static RequestDto toRequestDto(Request request) { requestDto.setCreated(request.getCreated()); requestDto.setEvent(request.getEvent().getId()); requestDto.setRequester(request.getRequester().getId()); + requestDto.setStatus(request.getStatus()); return requestDto; } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java index 8bb7537..74bee77 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java @@ -9,5 +9,7 @@ public interface RequestRepository extends JpaRepository { List findAllByRequester_Id(int userId); + List findAllByEvent_Id(int eventId); + // Integer countByEvent_IdAAndStatusEquals(int eventId, RequestStatus status); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java index 3dafb43..92ecfd4 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java @@ -11,4 +11,7 @@ public interface RequestService { List getRequestsByUserId(Integer userId); Request deleteRequest(Integer userId, Integer requestId); + + List getRequestsByEventId(Integer userId, Integer eventId); + } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java index d4eee71..08c9410 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java @@ -38,31 +38,32 @@ public Request createRequest(Integer userId, Integer eventId) { "Value: " + event.getState() ); } - if (event.getConfirmedRequests().equals(event.getParticipantLimit())) { - throw new ValidationException( + Integer confirmedRequests = event.getConfirmedRequests(); + if (confirmedRequests != null) { + if(confirmedRequests.equals(event.getParticipantLimit())){ + throw new ValidationException( "Field: event.state. Error: " + - "У события достигнут лимит запросов на участие. " + - "Value: " + event.getConfirmedRequests() - ); + "У события достигнут лимит запросов на участие. " + + "Value: " + confirmedRequests + ); + } } Request request = new Request(); User user = userService.getUserById(userId); request.setRequester(user); request.setEvent(event); - if (event.getRequestModeration()) { - request.setStatus(RequestStatus.PENDING); - } else { + request.setStatus(RequestStatus.PENDING); + if (!event.getRequestModeration()) { request.setStatus(RequestStatus.APPROVED); + event.setConfirmedRequests(confirmedRequests + 1); } request.setCreated(LocalDateTime.now()); - Request savedRequest = requestRepository.save(request); - return savedRequest; + return requestRepository.save(request); } @Override public List getRequestsByUserId(Integer userId) { - List requests = requestRepository.findAllByRequester_Id(userId); - return requests; + return requestRepository.findAllByRequester_Id(userId); } @Override @@ -72,7 +73,7 @@ public Request deleteRequest(Integer userId, Integer requestId) { new NotFoundException("Не найден запрос id=" + requestId)); if (!request.getRequester().getId().equals(userId)) { throw new ValidationException( - "Field: request.requestor.id. Error: " + + "Field: request.requester.id. Error: " + "Нельзя удалить чужой запрос. " + "Value: " + request.getRequester().getId() ); @@ -80,4 +81,17 @@ public Request deleteRequest(Integer userId, Integer requestId) { requestRepository.delete(request); return request; } + + @Override + public List getRequestsByEventId(Integer userId, Integer eventId) { + Event event = eventService.findEventById(eventId); + if (!event.getInitiator().getId().equals(userId)) { + throw new ValidationException( + "Field: event.initiator_id. " + + "Error: пользователь id=" + userId + " не является инициатором события id=" + eventId + + ". Value: " + event.getInitiator().getId() + ); + } + return requestRepository.findAllByEvent_Id(eventId); + } } From 02d9e9089b3c670ad13b42ab1f8a2138e76630c5 Mon Sep 17 00:00:00 2001 From: andrej1307 Date: Tue, 3 Jun 2025 16:31:31 +0700 Subject: [PATCH 12/29] =?UTF-8?q?feat:=20=D0=BF=D0=BE=D0=B4=D1=82=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B0?= =?UTF-8?q?=D1=8F=D0=B2=D0=BE=D0=BA.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../evmsevice/dto/RequestGroupDto.java | 17 +++++++ .../evmsevice/dto/RequestUpdateDto.java | 16 ++++++ .../evmsevice/enums/RequestStatus.java | 2 +- .../repository/RequestRepository.java | 7 ++- .../evmsevice/service/RequestService.java | 3 ++ .../evmsevice/service/RequestServiceImpl.java | 51 ++++++++++++++++++- 6 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestGroupDto.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestUpdateDto.java diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestGroupDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestGroupDto.java new file mode 100644 index 0000000..937959a --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestGroupDto.java @@ -0,0 +1,17 @@ +package ru.practicum.evmsevice.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@Setter +@Getter +@NoArgsConstructor +@ToString +public class RequestGroupDto { + List confirmedRequests; + List rejectedRequests; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestUpdateDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestUpdateDto.java new file mode 100644 index 0000000..544cc0a --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestUpdateDto.java @@ -0,0 +1,16 @@ +package ru.practicum.evmsevice.dto; + +import lombok.*; +import ru.practicum.evmsevice.enums.RequestStatus; + +import java.util.ArrayList; +import java.util.List; + +@Setter +@Getter +@NoArgsConstructor +@ToString +public class RequestUpdateDto { + List requestIds = new ArrayList<>(); + RequestStatus status; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/enums/RequestStatus.java b/ewm-service/src/main/java/ru/practicum/evmsevice/enums/RequestStatus.java index 0137965..575137f 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/enums/RequestStatus.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/enums/RequestStatus.java @@ -4,7 +4,7 @@ public enum RequestStatus { PENDING, - APPROVED, + CONFIRMED, REJECTED, CANCELED; diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java index 74bee77..a3670b1 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java @@ -1,6 +1,8 @@ package ru.practicum.evmsevice.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import ru.practicum.evmsevice.enums.RequestStatus; import ru.practicum.evmsevice.model.Request; @@ -11,5 +13,8 @@ public interface RequestRepository extends JpaRepository { List findAllByEvent_Id(int eventId); - // Integer countByEvent_IdAAndStatusEquals(int eventId, RequestStatus status); + @Query("UPDATE Request r SET r.status = :status WHERE r.id IN :ids ") + void updateStatus(@Param("status") RequestStatus status, @Param("ids") List ids); + + List findAllByIdIsIn(List ids); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java index 92ecfd4..1421a68 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java @@ -1,5 +1,7 @@ package ru.practicum.evmsevice.service; +import ru.practicum.evmsevice.dto.RequestGroupDto; +import ru.practicum.evmsevice.enums.RequestStatus; import ru.practicum.evmsevice.model.Event; import ru.practicum.evmsevice.model.Request; @@ -14,4 +16,5 @@ public interface RequestService { List getRequestsByEventId(Integer userId, Integer eventId); + RequestGroupDto updateRequestsStatus(Integer eventId, List requestIds, RequestStatus status); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java index 08c9410..4235370 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java @@ -3,10 +3,12 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import ru.practicum.evmsevice.dto.RequestGroupDto; import ru.practicum.evmsevice.enums.EventState; import ru.practicum.evmsevice.enums.RequestStatus; import ru.practicum.evmsevice.exception.NotFoundException; import ru.practicum.evmsevice.exception.ValidationException; +import ru.practicum.evmsevice.mapper.RequestMapper; import ru.practicum.evmsevice.model.*; import ru.practicum.evmsevice.repository.RequestRepository; @@ -54,7 +56,7 @@ public Request createRequest(Integer userId, Integer eventId) { request.setEvent(event); request.setStatus(RequestStatus.PENDING); if (!event.getRequestModeration()) { - request.setStatus(RequestStatus.APPROVED); + request.setStatus(RequestStatus.CONFIRMED); event.setConfirmedRequests(confirmedRequests + 1); } request.setCreated(LocalDateTime.now()); @@ -94,4 +96,51 @@ public List getRequestsByEventId(Integer userId, Integer eventId) { } return requestRepository.findAllByEvent_Id(eventId); } + + @Override + public RequestGroupDto updateRequestsStatus(Integer eventId, List requestIds, RequestStatus status) { + if (requestIds.size() == 0) { + throw new ValidationException( + "Field: requestIds.size. " + + "Error: список идентификаторов запроса пустой. " + + "Value: " + requestIds.size() + ); + } + Event event = eventService.findEventById(eventId); + if (event.getRequestModeration() || (event.getParticipantLimit() != null)) { + if(event.getParticipantLimit().equals(event.getConfirmedRequests())) { + throw new ValidationException( + "Field: event.confirmedRequests. " + + "Error: Достигнуто максимальное количество заявок для события id=" + eventId + + ". Value: " + event.getInitiator().getId() + ); + } + } + List requests = requestRepository.findAllByIdIsIn(requestIds); + requestRepository.updateStatus(status, requestIds); + // проверякм статус заявок + for (Request request : requests) { + if(request.getStatus() != RequestStatus.PENDING) { + throw new ValidationException( + "Field: requestIds.sstatus. " + + "Error: Невозможно изменить статус запроса id=" + request.getId() + + "Value: " + request.getStatus() + ); + } + } + RequestGroupDto requestGroupDto = new RequestGroupDto(); +/* requestGroupDto.setConfirmedRequests( + requestRepository.findAllByStatus(RequestStatus.CONFIRMED) + .stream() + .map(RequestMapper::toRequestDto) + .toList() + ); + requestGroupDto.setRejectedRequests( + requestRepository.findAllByStatus(RequestStatus.CONFIRMED) + .stream() + .map(RequestMapper::toRequestDto) + .toList() + ); */ + return requestGroupDto; + } } From 9eb73264426832b07d319380105a01697c28a4f2 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Tue, 3 Jun 2025 22:14:25 +0700 Subject: [PATCH 13/29] =?UTF-8?q?fix:=20=D0=BF=D0=BE=D0=B4=D1=82=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=81=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../evmsevice/controller/UserController.java | 29 ++-- .../evmsevice/dto/RequestGroupDto.java | 5 +- .../evmsevice/service/RequestService.java | 3 +- .../evmsevice/service/RequestServiceImpl.java | 128 +++++++++++++----- 4 files changed, 117 insertions(+), 48 deletions(-) diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java index f04c521..5599271 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java @@ -7,18 +7,17 @@ import org.springframework.web.bind.annotation.*; import ru.practicum.evmsevice.dto.*; import ru.practicum.evmsevice.mapper.RequestMapper; -import ru.practicum.evmsevice.model.Event; import ru.practicum.evmsevice.model.Request; import ru.practicum.evmsevice.service.EventService; import ru.practicum.evmsevice.service.RequestService; -import ru.practicum.evmsevice.service.UserService; import java.util.List; @Slf4j @RequiredArgsConstructor @RestController -@RequestMapping("/users")public class UserController { +@RequestMapping("/users") +public class UserController { private final EventService eventService; private final RequestService requestService; @@ -43,8 +42,8 @@ public EventFullDto getEvent(@PathVariable int userId, @GetMapping("/{userId}/events") @ResponseStatus(HttpStatus.OK) public List getEvents(@PathVariable int userId, - @RequestParam(name = "from", defaultValue = "0") Integer from, - @RequestParam(name = "size", defaultValue = "10") Integer size) { + @RequestParam(name = "from", defaultValue = "0") Integer from, + @RequestParam(name = "size", defaultValue = "10") Integer size) { log.info("Пользователь id={} запрашивает информацию об инциированных событиях.", userId); return eventService.getEventsByUserId(userId, from, size); } @@ -71,10 +70,20 @@ public List findRequestsByEventId(@PathVariable int userId, .toList(); } - @PostMapping("/{userId}/requests") + @PatchMapping("/{userId}/events/{eventId}/requests") + @ResponseStatus(HttpStatus.OK) + public RequestGroupDto patchRequestsByEventId(@PathVariable int userId, + @PathVariable int eventId, + @RequestBody RequestUpdateDto requestUpdateDto) { + log.info("Пользователь id={} модерирует запросы на событие id={}.", + userId, eventId); + return requestService.updateRequestsStatus(userId, eventId, requestUpdateDto); + } + + @PostMapping("/{userId}/requests") @ResponseStatus(HttpStatus.CREATED) - public RequestDto createRequet(@PathVariable Integer userId, - @RequestParam(name = "eventId", required = true) Integer eventId) { + public RequestDto createRequest(@PathVariable Integer userId, + @RequestParam(name = "eventId", required = true) Integer eventId) { log.info("Пользователь id={} создает запрос на участие в событии id={}.", userId, eventId); Request request = requestService.createRequest(userId, eventId); @@ -93,8 +102,8 @@ public List findRequestsByUserId(@PathVariable Integer userId) { @DeleteMapping("/{userId}/requests/{requestId}/cancel") @ResponseStatus(HttpStatus.OK) - public RequestDto deleteRequestById( @PathVariable Integer userId, - @PathVariable Integer requestId) { + public RequestDto deleteRequestById(@PathVariable Integer userId, + @PathVariable Integer requestId) { log.info("Пользователь id={} удаляет запрос id={}.", userId, requestId); return RequestMapper.toRequestDto(requestService.deleteRequest(userId, requestId)); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestGroupDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestGroupDto.java index 937959a..5ea6364 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestGroupDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestGroupDto.java @@ -5,6 +5,7 @@ import lombok.Setter; import lombok.ToString; +import java.util.ArrayList; import java.util.List; @Setter @@ -12,6 +13,6 @@ @NoArgsConstructor @ToString public class RequestGroupDto { - List confirmedRequests; - List rejectedRequests; + List confirmedRequests = new ArrayList<>(); + List rejectedRequests = new ArrayList<>(); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java index 1421a68..3ca5d19 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java @@ -1,6 +1,7 @@ package ru.practicum.evmsevice.service; import ru.practicum.evmsevice.dto.RequestGroupDto; +import ru.practicum.evmsevice.dto.RequestUpdateDto; import ru.practicum.evmsevice.enums.RequestStatus; import ru.practicum.evmsevice.model.Event; import ru.practicum.evmsevice.model.Request; @@ -16,5 +17,5 @@ public interface RequestService { List getRequestsByEventId(Integer userId, Integer eventId); - RequestGroupDto updateRequestsStatus(Integer eventId, List requestIds, RequestStatus status); + RequestGroupDto updateRequestsStatus(Integer userId, Integer eventId, RequestUpdateDto requestUpdateDto); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java index 4235370..b068542 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java @@ -4,15 +4,19 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.practicum.evmsevice.dto.RequestGroupDto; +import ru.practicum.evmsevice.dto.RequestUpdateDto; import ru.practicum.evmsevice.enums.EventState; import ru.practicum.evmsevice.enums.RequestStatus; import ru.practicum.evmsevice.exception.NotFoundException; import ru.practicum.evmsevice.exception.ValidationException; import ru.practicum.evmsevice.mapper.RequestMapper; -import ru.practicum.evmsevice.model.*; +import ru.practicum.evmsevice.model.Event; +import ru.practicum.evmsevice.model.Request; +import ru.practicum.evmsevice.model.User; import ru.practicum.evmsevice.repository.RequestRepository; import java.time.LocalDateTime; +import java.util.Collections; import java.util.List; @Service @@ -29,24 +33,24 @@ public Request createRequest(Integer userId, Integer eventId) { if (event.getInitiator().getId().equals(userId)) { throw new ValidationException( "Field: event.initiator_id. Error: " + - "Инициатор события не может добавить запрос на участие в своём событии. " + - "Value: " + event.getInitiator().getId() + "Инициатор события не может добавить запрос на участие в своём событии. " + + "Value: " + event.getInitiator().getId() ); } if (!event.getState().equals(EventState.PUBLISHED)) { throw new ValidationException( "Field: event.state. Error: " + - "Нельзя участвовать в неопубликованном событии. " + - "Value: " + event.getState() + "Нельзя участвовать в неопубликованном событии. " + + "Value: " + event.getState() ); } Integer confirmedRequests = event.getConfirmedRequests(); if (confirmedRequests != null) { - if(confirmedRequests.equals(event.getParticipantLimit())){ + if (confirmedRequests.equals(event.getParticipantLimit())) { throw new ValidationException( - "Field: event.state. Error: " + - "У события достигнут лимит запросов на участие. " + - "Value: " + confirmedRequests + "Field: event.state. Error: " + + "У события достигнут лимит запросов на участие. " + + "Value: " + confirmedRequests ); } } @@ -84,6 +88,9 @@ public Request deleteRequest(Integer userId, Integer requestId) { return request; } + /** + * Метод изменения поиска запросов к событию + */ @Override public List getRequestsByEventId(Integer userId, Integer eventId) { Event event = eventService.findEventById(eventId); @@ -97,18 +104,31 @@ public List getRequestsByEventId(Integer userId, Integer eventId) { return requestRepository.findAllByEvent_Id(eventId); } + /** + * Метод изменения статуса запросов + */ @Override - public RequestGroupDto updateRequestsStatus(Integer eventId, List requestIds, RequestStatus status) { - if (requestIds.size() == 0) { + public RequestGroupDto updateRequestsStatus(Integer userId, Integer eventId, RequestUpdateDto requestUpdateDto) { + Event event = eventService.findEventById(eventId); + if (!event.getInitiator().getId().equals(userId)) { + throw new ValidationException( + "Field: event.initiator_id. Error: " + + "Пользователь id=" + userId + " не является инициатором события id=" + eventId + + ". Value: " + event.getInitiator().getId() + ); + } + + if (requestUpdateDto.getRequestIds().isEmpty()) { throw new ValidationException( "Field: requestIds.size. " + "Error: список идентификаторов запроса пустой. " + - "Value: " + requestIds.size() + "Value: 0." ); } - Event event = eventService.findEventById(eventId); - if (event.getRequestModeration() || (event.getParticipantLimit() != null)) { - if(event.getParticipantLimit().equals(event.getConfirmedRequests())) { + + if (event.getRequestModeration() + || ((event.getParticipantLimit() != null) && (event.getParticipantLimit() > 0))) { + if (event.getParticipantLimit().equals(event.getConfirmedRequests())) { throw new ValidationException( "Field: event.confirmedRequests. " + "Error: Достигнуто максимальное количество заявок для события id=" + eventId + @@ -116,31 +136,69 @@ public RequestGroupDto updateRequestsStatus(Integer eventId, List reque ); } } - List requests = requestRepository.findAllByIdIsIn(requestIds); - requestRepository.updateStatus(status, requestIds); - // проверякм статус заявок - for (Request request : requests) { - if(request.getStatus() != RequestStatus.PENDING) { + RequestGroupDto requestGroupDto = new RequestGroupDto(); + RequestStatus status = requestUpdateDto.getStatus(); + List requestIds = requestUpdateDto.getRequestIds(); + System.out.printf("==%s\n", requestIds.toString()); + + // Если запросы откланены меняем без проверок статус всех запросов + if (status == RequestStatus.REJECTED) { + requestRepository.updateStatus(status, requestIds); + requestGroupDto.setRejectedRequests( + requestRepository.findAllByIdIsIn(requestIds) + .stream() + .map(RequestMapper::toRequestDto) + .toList() + ); + return requestGroupDto; + } + + Integer participantLimit = 0; + if (event.getParticipantLimit() != null) { + participantLimit = event.getParticipantLimit(); + } + // Если лимит участников не установлен, то меняем статус у всех сразу + if (participantLimit == 0) { + requestRepository.updateStatus(status, requestIds); + requestGroupDto.setConfirmedRequests( + requestRepository.findAllByIdIsIn(requestIds) + .stream() + .map(RequestMapper::toRequestDto) + .toList() + ); + return requestGroupDto; + } + + Integer ConfirmedRequests = 0; + if(event.getConfirmedRequests() != null) { + ConfirmedRequests = event.getConfirmedRequests(); + } + Collections.sort(requestIds); + for (int i = 0; i < requestIds.size(); i++) { + System.out.printf("* i=%d\n", i); + Integer requestId = requestIds.get(i); + Request request = requestRepository.findById(requestId) + .orElseThrow(() -> + new NotFoundException("Не найден запрос id=" + requestId)); + if (request.getStatus() != RequestStatus.PENDING) { throw new ValidationException( - "Field: requestIds.sstatus. " + + "Field: requestIds.status. " + "Error: Невозможно изменить статус запроса id=" + request.getId() + - "Value: " + request.getStatus() + "Value: " + request.getStatus() ); } + if (!event.getConfirmedRequests().equals(event.getParticipantLimit())) { + request.setStatus(status); + requestRepository.save(request); + requestGroupDto.getConfirmedRequests().add(RequestMapper.toRequestDto(request)); + event.setConfirmedRequests(event.getConfirmedRequests() + 1); + } else { + status = RequestStatus.REJECTED; + request.setStatus(status); + requestRepository.save(request); + requestGroupDto.getRejectedRequests().add(RequestMapper.toRequestDto(request)); + } } - RequestGroupDto requestGroupDto = new RequestGroupDto(); -/* requestGroupDto.setConfirmedRequests( - requestRepository.findAllByStatus(RequestStatus.CONFIRMED) - .stream() - .map(RequestMapper::toRequestDto) - .toList() - ); - requestGroupDto.setRejectedRequests( - requestRepository.findAllByStatus(RequestStatus.CONFIRMED) - .stream() - .map(RequestMapper::toRequestDto) - .toList() - ); */ return requestGroupDto; } } From cda0629c75159c38b847bea2466f825578b23a78 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Fri, 6 Jun 2025 06:56:32 +0700 Subject: [PATCH 14/29] =?UTF-8?q?fix:=20=D0=BF=D0=BE=D0=B4=D1=82=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=81=D0=BE=D0=B2=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../practicum/evmsevice/dto/NewEventDto.java | 1 + .../evmsevice/mapper/EventMapper.java | 13 +-- .../evmsevice/service/RequestServiceImpl.java | 96 +++++++------------ 3 files changed, 41 insertions(+), 69 deletions(-) diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewEventDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewEventDto.java index a8c0b3d..d50b20f 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewEventDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewEventDto.java @@ -26,6 +26,7 @@ public class NewEventDto private String description; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime eventDate; + @NotNull(message = "Место события должно быть определено.") private Location location; private Boolean paid; private Integer participantLimit; diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java index ec53933..35328cd 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java @@ -20,16 +20,17 @@ public static Event toEvent(final NewEventDto newDto) { event.setCreatedOn(LocalDateTime.now()); event.setLat(newDto.getLocation().getLat()); event.setLon(newDto.getLocation().getLon()); - if(newDto.getPaid()) { + event.setPaid(false); + if(newDto.getPaid() != null) { event.setPaid(newDto.getPaid()); - } else { - event.setPaid(false); } - event.setParticipantLimit(newDto.getParticipantLimit()); + event.setParticipantLimit(0); + if(newDto.getParticipantLimit() != null) { + event.setParticipantLimit(newDto.getParticipantLimit()); + } + event.setRequestModeration(false); if (newDto.getRequestModeration() != null) { event.setRequestModeration(newDto.getRequestModeration()); - } else { - event.setRequestModeration(false); } event.setState(EventState.PENDING); event.setTitle(newDto.getTitle()); diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java index b068542..50d9435 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java @@ -118,85 +118,55 @@ public RequestGroupDto updateRequestsStatus(Integer userId, Integer eventId, Req ); } - if (requestUpdateDto.getRequestIds().isEmpty()) { + // ...нельзя подтвердить заявку, если уже достигнут лимит по заявкам на данное событие + // (Ожидается код ошибки 409) + if ((event.getParticipantLimit() > 0) + && event.getParticipantLimit().equals(event.getConfirmedRequests())){ throw new ValidationException( - "Field: requestIds.size. " + - "Error: список идентификаторов запроса пустой. " + - "Value: 0." - ); - } - - if (event.getRequestModeration() - || ((event.getParticipantLimit() != null) && (event.getParticipantLimit() > 0))) { - if (event.getParticipantLimit().equals(event.getConfirmedRequests())) { - throw new ValidationException( "Field: event.confirmedRequests. " + "Error: Достигнуто максимальное количество заявок для события id=" + eventId + - ". Value: " + event.getInitiator().getId() - ); - } + ". Value: " + event.getConfirmedRequests() + ); } + RequestGroupDto requestGroupDto = new RequestGroupDto(); - RequestStatus status = requestUpdateDto.getStatus(); List requestIds = requestUpdateDto.getRequestIds(); - System.out.printf("==%s\n", requestIds.toString()); - - // Если запросы откланены меняем без проверок статус всех запросов - if (status == RequestStatus.REJECTED) { - requestRepository.updateStatus(status, requestIds); - requestGroupDto.setRejectedRequests( - requestRepository.findAllByIdIsIn(requestIds) - .stream() - .map(RequestMapper::toRequestDto) - .toList() - ); + if (requestIds.isEmpty()) { return requestGroupDto; } + Collections.sort(requestIds); + RequestStatus status = requestUpdateDto.getStatus(); - Integer participantLimit = 0; - if (event.getParticipantLimit() != null) { - participantLimit = event.getParticipantLimit(); - } - // Если лимит участников не установлен, то меняем статус у всех сразу - if (participantLimit == 0) { - requestRepository.updateStatus(status, requestIds); - requestGroupDto.setConfirmedRequests( - requestRepository.findAllByIdIsIn(requestIds) - .stream() - .map(RequestMapper::toRequestDto) - .toList() - ); - return requestGroupDto; + Integer confirmedRequests = 0; + if (event.getConfirmedRequests() != null) { + confirmedRequests = event.getConfirmedRequests(); } - Integer ConfirmedRequests = 0; - if(event.getConfirmedRequests() != null) { - ConfirmedRequests = event.getConfirmedRequests(); - } - Collections.sort(requestIds); - for (int i = 0; i < requestIds.size(); i++) { - System.out.printf("* i=%d\n", i); - Integer requestId = requestIds.get(i); + // Проверяем заявки из списка + for (Integer requestId : requestIds) { Request request = requestRepository.findById(requestId) - .orElseThrow(() -> - new NotFoundException("Не найден запрос id=" + requestId)); - if (request.getStatus() != RequestStatus.PENDING) { + .orElseThrow(() -> new NotFoundException("Не наайдена заявка id=" + requestId)); + // ... статус можно изменить только у заявок, находящихся в состоянии ожидания + // (Ожидается код ошибки 409) + if (!request.getStatus().equals(RequestStatus.PENDING)) { throw new ValidationException( - "Field: requestIds.status. " + - "Error: Невозможно изменить статус запроса id=" + request.getId() + - "Value: " + request.getStatus() + "Field: request.status. " + + "Error: недопустимый статус заявки id=" + requestId + + ". Value: " + request.getStatus() ); } - if (!event.getConfirmedRequests().equals(event.getParticipantLimit())) { - request.setStatus(status); - requestRepository.save(request); - requestGroupDto.getConfirmedRequests().add(RequestMapper.toRequestDto(request)); - event.setConfirmedRequests(event.getConfirmedRequests() + 1); - } else { + // ... если при подтверждении данной заявки, лимит заявок для события исчерпан, + // то все неподтверждённые заявки необходимо отклонить + if (confirmedRequests.equals(event.getParticipantLimit())) { status = RequestStatus.REJECTED; - request.setStatus(status); - requestRepository.save(request); - requestGroupDto.getRejectedRequests().add(RequestMapper.toRequestDto(request)); + } + request.setStatus(status); + Request savedRequest = requestRepository.save(request); + if (savedRequest.getStatus().equals(RequestStatus.CONFIRMED)) { + requestGroupDto.getConfirmedRequests().add(RequestMapper.toRequestDto(savedRequest)); + confirmedRequests++; + } else if (savedRequest.getStatus().equals(RequestStatus.REJECTED)) { + requestGroupDto.getRejectedRequests().add(RequestMapper.toRequestDto(savedRequest)); } } return requestGroupDto; From c88b518027a0c97db0ec158f32c64f0f8f708c21 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Sun, 8 Jun 2025 22:06:04 +0700 Subject: [PATCH 15/29] =?UTF-8?q?feat:=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B=20=D0=B7=D0=B0=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=81=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=81=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BF=D1=80=D0=BE=D1=81=D0=BC=D0=BE=D1=82=D1=80?= =?UTF-8?q?=D0=B0=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../evmsevice/client/StatsClient.java | 16 +++++++ .../evmsevice/controller/ErrorAdvisor.java | 14 ++++++ .../controller/PublicController.java | 45 +++++++++++++++++++ .../exception/DataConflictException.java | 7 +++ .../evmsevice/mapper/EventMapper.java | 4 ++ .../repository/RequestRepository.java | 6 +-- .../evmsevice/service/EventServiceImpl.java | 14 ++++-- .../evmsevice/service/RequestServiceImpl.java | 6 ++- .../ru/practicum/statclient/BaseClient.java | 31 ++++++++++++- .../ru/practicum/statdto/StatsDtoList.java | 14 ++++++ .../statsvc/controller/StatController.java | 8 +++- 11 files changed, 152 insertions(+), 13 deletions(-) create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/exception/DataConflictException.java create mode 100644 stats-server/stat-dto/src/main/java/ru/practicum/statdto/StatsDtoList.java diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java b/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java index c368f0c..e0ce084 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java @@ -11,14 +11,20 @@ import org.springframework.web.util.DefaultUriBuilderFactory; import ru.practicum.statclient.BaseClient; import ru.practicum.statdto.HitDto; +import ru.practicum.statdto.StatsDto; +import ru.practicum.statdto.StatsDtoList; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Map; @Component public class StatsClient extends BaseClient { private static final String PREFIX_HIT = "/hit"; private static final String PREFIX_STATS = "/stats"; + private static final String PREFIX_EVETS = "/events/"; @Autowired public StatsClient(@Value("${statserver.url}") String serverUrl, RestTemplateBuilder builder) { @@ -46,4 +52,14 @@ public void hitInfo(String appName, HttpServletRequest request) { hitDto.setTimestamp(LocalDateTime.now()); post(hitDto); } + + public Integer getEventViews(Integer eventId, Boolean unique) { + Map parameters = Map.of("uris", PREFIX_EVETS + eventId, + "unique", unique); + List dtos = getStatsList(PREFIX_STATS, parameters); + if (dtos.isEmpty()) { + return 0; + } + return dtos.get(0).getHits(); + } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java index c443421..0fac822 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java @@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import ru.practicum.evmsevice.dto.ApiError; +import ru.practicum.evmsevice.exception.DataConflictException; import ru.practicum.evmsevice.exception.InternalServerException; import ru.practicum.evmsevice.exception.NotFoundException; import ru.practicum.evmsevice.exception.ValidationException; @@ -61,6 +62,19 @@ public ApiError onInternalException(final InternalServerException e) { return apiError; } + @ExceptionHandler(DataConflictException.class) + @ResponseStatus(HttpStatus.CONFLICT) + public ApiError onDataIntegrityViolationException(final DataConflictException e) { + log.error("409 {}", e.getMessage()); + ApiError apiError = new ApiError(); + apiError.setStatus(HttpStatus.CONFLICT); + apiError.setReason("Конфликт данных."); + apiError.setMessage(e.getMessage()); + apiError.setTimestamp(LocalDateTime.now()); + return apiError; + } + + @ExceptionHandler(DataIntegrityViolationException.class) @ResponseStatus(HttpStatus.CONFLICT) public ApiError onDataIntegrityViolationException(final DataIntegrityViolationException e) { diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java new file mode 100644 index 0000000..2bcd7f7 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java @@ -0,0 +1,45 @@ +package ru.practicum.evmsevice.controller; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.evmsevice.client.StatsClient; +import ru.practicum.evmsevice.dto.EventFullDto; +import ru.practicum.evmsevice.enums.EventState; +import ru.practicum.evmsevice.exception.NotFoundException; +import ru.practicum.evmsevice.mapper.EventMapper; +import ru.practicum.evmsevice.model.Event; +import ru.practicum.evmsevice.service.EventService; + +import static ru.practicum.evmsevice.mapper.EventMapper.toFullDto; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping() +public class PublicController { + @Value("${spring.application.name}") + private String appName; + private final StatsClient statsClient; + private final EventService eventService; + + @GetMapping("/events/{id}") + @ResponseStatus(HttpStatus.OK) + public EventFullDto findEventById(@PathVariable("id") int id, + HttpServletRequest request) { + log.info("Пользователь просматривает событие: {}", id); + Event event = eventService.findEventById(id); + // Событие должно быть опубликовано + if (!event.getState().equals(EventState.PUBLISHED)) { + throw new NotFoundException("Среди опубликованных не найдено событие id=" + id); + } + // сохраняем запрос в сервере статистики + statsClient.hitInfo(appName, request); + EventFullDto eventFullDto = EventMapper.toFullDto(event); + eventFullDto.setViews(statsClient.getEventViews(id, true)); + return eventFullDto; + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/exception/DataConflictException.java b/ewm-service/src/main/java/ru/practicum/evmsevice/exception/DataConflictException.java new file mode 100644 index 0000000..a72ed72 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/exception/DataConflictException.java @@ -0,0 +1,7 @@ +package ru.practicum.evmsevice.exception; + +public class DataConflictException extends RuntimeException { + public DataConflictException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java index 35328cd..19f9e7c 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java @@ -48,6 +48,10 @@ public static EventFullDto toFullDto(Event event) { dto.setInitiator(UserMapper.toUserShortDto(event.getInitiator())); dto.setLocation(new Location(event.getLat(), event.getLon())); dto.setParticipantLimit(event.getParticipantLimit()); + dto.setConfirmedRequests(0); + if(event.getConfirmedRequests() != null) { + dto.setConfirmedRequests(event.getConfirmedRequests()); + } dto.setRequestModeration(event.getRequestModeration()); dto.setState(event.getState()); dto.setPaid(event.getPaid()); diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java index a3670b1..c17296b 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java @@ -13,8 +13,6 @@ public interface RequestRepository extends JpaRepository { List findAllByEvent_Id(int eventId); - @Query("UPDATE Request r SET r.status = :status WHERE r.id IN :ids ") - void updateStatus(@Param("status") RequestStatus status, @Param("ids") List ids); - - List findAllByIdIsIn(List ids); + @Query("SELECT COUNT(r) FROM Request r WHERE r.event.id = :eventId AND r.status = 'CONFIRMED'") + Integer getCountConfirmedRequestsByEventId(@Param("eventId") int eventId); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java index 1f638b3..b82aeef 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import ru.practicum.evmsevice.client.StatsClient; import ru.practicum.evmsevice.dto.*; import ru.practicum.evmsevice.enums.EventAdminAction; import ru.practicum.evmsevice.enums.EventState; @@ -14,6 +15,7 @@ import ru.practicum.evmsevice.model.Event; import ru.practicum.evmsevice.model.User; import ru.practicum.evmsevice.repository.EventRepository; +import ru.practicum.evmsevice.repository.RequestRepository; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -27,8 +29,10 @@ public class EventServiceImpl implements EventService{ private static final Integer HOURS_EVENT_DELAY = 2; private final EventRepository eventRepository; + private final RequestRepository requestRepository; private final UserService userService; private final CategoryService categoryService; + private final StatsClient statsClient; /** * Создание нового события @@ -56,14 +60,15 @@ public EventFullDto createEvent(NewEventDto newEventDto, Integer userId) { @Override public EventFullDto getEventById(Integer eventId, Integer userId) { - Event event = eventRepository.findById(eventId) - .orElseThrow(() -> - new NotFoundException("Не найдено событие id=" + eventId)); + Event event = findEventById(eventId); if (!event.getInitiator().getId().equals(userId)) { throw new ValidationException("Пользователь id=" + userId + " не является инициатором события id=" + eventId); } - return EventMapper.toFullDto(event); + event.setConfirmedRequests(requestRepository.getCountConfirmedRequestsByEventId(eventId)); + EventFullDto eventFullDto = EventMapper.toFullDto(event); + eventFullDto.setViews(statsClient.getEventViews(eventId, true)); + return eventFullDto; } @Override @@ -215,6 +220,7 @@ public Event findEventById(Integer eventId) { Event event = eventRepository.findById(eventId) .orElseThrow(() -> new NotFoundException("Не найдено событие id=" + eventId)); + event.setConfirmedRequests(requestRepository.getCountConfirmedRequestsByEventId(eventId)); return event; } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java index 50d9435..343fdc3 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java @@ -1,12 +1,14 @@ package ru.practicum.evmsevice.service; import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.practicum.evmsevice.dto.RequestGroupDto; import ru.practicum.evmsevice.dto.RequestUpdateDto; import ru.practicum.evmsevice.enums.EventState; import ru.practicum.evmsevice.enums.RequestStatus; +import ru.practicum.evmsevice.exception.DataConflictException; import ru.practicum.evmsevice.exception.NotFoundException; import ru.practicum.evmsevice.exception.ValidationException; import ru.practicum.evmsevice.mapper.RequestMapper; @@ -122,7 +124,7 @@ public RequestGroupDto updateRequestsStatus(Integer userId, Integer eventId, Req // (Ожидается код ошибки 409) if ((event.getParticipantLimit() > 0) && event.getParticipantLimit().equals(event.getConfirmedRequests())){ - throw new ValidationException( + throw new DataConflictException( "Field: event.confirmedRequests. " + "Error: Достигнуто максимальное количество заявок для события id=" + eventId + ". Value: " + event.getConfirmedRequests() @@ -149,7 +151,7 @@ public RequestGroupDto updateRequestsStatus(Integer userId, Integer eventId, Req // ... статус можно изменить только у заявок, находящихся в состоянии ожидания // (Ожидается код ошибки 409) if (!request.getStatus().equals(RequestStatus.PENDING)) { - throw new ValidationException( + throw new DataConflictException( "Field: request.status. " + "Error: недопустимый статус заявки id=" + requestId + ". Value: " + request.getStatus() diff --git a/stats-server/stat-client/src/main/java/ru/practicum/statclient/BaseClient.java b/stats-server/stat-client/src/main/java/ru/practicum/statclient/BaseClient.java index 28ce7fc..bfc5406 100644 --- a/stats-server/stat-client/src/main/java/ru/practicum/statclient/BaseClient.java +++ b/stats-server/stat-client/src/main/java/ru/practicum/statclient/BaseClient.java @@ -5,7 +5,10 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestTemplate; +import ru.practicum.statdto.StatsDto; +import ru.practicum.statdto.StatsDtoList; +import java.util.List; import java.util.Map; public class BaseClient { @@ -52,7 +55,11 @@ protected ResponseEntity makeAndSendRequest(HttpMethod method, stringParametrs.append(key); stringParametrs.append("}&"); } - serverResponse = rest.exchange(stringParametrs.toString(), method, requestEntity, Object.class, parameters); + serverResponse = rest.exchange(stringParametrs.toString(), + method, + requestEntity, + Object.class, + parameters); } else { serverResponse = rest.exchange(path, method, requestEntity, Object.class); } @@ -61,4 +68,26 @@ protected ResponseEntity makeAndSendRequest(HttpMethod method, } return prepareClientResponse(serverResponse); } + + protected List getStatsList(String path, + Map parameters) { + StatsDtoList serverResponse = new StatsDtoList(); + try { + if (parameters != null) { + StringBuilder stringParametrs = new StringBuilder(path); + stringParametrs.append("?"); + for (String key : parameters.keySet()) { + stringParametrs.append(key); + stringParametrs.append("={"); + stringParametrs.append(key); + stringParametrs.append("}&"); + } + serverResponse = rest.getForObject(stringParametrs.toString(), StatsDtoList.class, parameters); + } + } catch (Exception e) { + e.printStackTrace(); + return List.of(); + } + return serverResponse.getStatsDtos(); + } } diff --git a/stats-server/stat-dto/src/main/java/ru/practicum/statdto/StatsDtoList.java b/stats-server/stat-dto/src/main/java/ru/practicum/statdto/StatsDtoList.java new file mode 100644 index 0000000..c2a8440 --- /dev/null +++ b/stats-server/stat-dto/src/main/java/ru/practicum/statdto/StatsDtoList.java @@ -0,0 +1,14 @@ +package ru.practicum.statdto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Setter +@Getter +@NoArgsConstructor +public class StatsDtoList { + private List statsDtos; +} diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/controller/StatController.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/controller/StatController.java index 98bb153..85165c6 100644 --- a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/controller/StatController.java +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/controller/StatController.java @@ -6,6 +6,7 @@ import org.springframework.web.bind.annotation.*; import ru.practicum.statdto.HitDto; import ru.practicum.statdto.StatsDto; +import ru.practicum.statdto.StatsDtoList; import ru.practicum.statsvc.service.StatService; import java.util.List; @@ -27,13 +28,16 @@ public void hit(@RequestBody HitDto dto) { @GetMapping("/stats") @ResponseStatus(HttpStatus.OK) - public List getStats( + public StatsDtoList getStats( @RequestParam(required = false) String start, @RequestParam(required = false) String end, @RequestParam(required = false) List uris, @RequestParam(defaultValue = "false") Boolean unique, @RequestParam(defaultValue = "10") Integer size) { log.info("Запрашивается информация о посещении эндпоинта {} с {} до {}.", uris, start, end); - return statService.getStats(start, end, uris, unique, size); + List statDtos = statService.getStats(start, end, uris, unique, size); + StatsDtoList statsDtoList = new StatsDtoList(); + statsDtoList.setStatsDtos(statDtos); + return statsDtoList; } } From db48cfd3b11edc71006d9a2cf481a69fd06458b0 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Mon, 9 Jun 2025 23:39:00 +0700 Subject: [PATCH 16/29] =?UTF-8?q?feat:=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PublicController.java | 34 ++++++++++++++++++- .../evmsevice/repository/EventRepository.java | 4 ++- .../repository/EventSpecification.java | 23 +++++++++++++ .../evmsevice/service/EventService.java | 9 +++++ .../evmsevice/service/EventServiceImpl.java | 29 ++++++++++++++++ 5 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventSpecification.java diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java index 2bcd7f7..68ea5bf 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java @@ -8,12 +8,16 @@ import org.springframework.web.bind.annotation.*; import ru.practicum.evmsevice.client.StatsClient; import ru.practicum.evmsevice.dto.EventFullDto; +import ru.practicum.evmsevice.dto.EventShortDto; import ru.practicum.evmsevice.enums.EventState; import ru.practicum.evmsevice.exception.NotFoundException; import ru.practicum.evmsevice.mapper.EventMapper; import ru.practicum.evmsevice.model.Event; import ru.practicum.evmsevice.service.EventService; +import java.util.ArrayList; +import java.util.List; + import static ru.practicum.evmsevice.mapper.EventMapper.toFullDto; @Slf4j @@ -30,7 +34,7 @@ public class PublicController { @ResponseStatus(HttpStatus.OK) public EventFullDto findEventById(@PathVariable("id") int id, HttpServletRequest request) { - log.info("Пользователь просматривает событие: {}", id); + log.info("Пользователь запрвшмвает для просмотра событие: {}", id); Event event = eventService.findEventById(id); // Событие должно быть опубликовано if (!event.getState().equals(EventState.PUBLISHED)) { @@ -42,4 +46,32 @@ public EventFullDto findEventById(@PathVariable("id") int id, eventFullDto.setViews(statsClient.getEventViews(id, true)); return eventFullDto; } + + @GetMapping("/events") + @ResponseStatus(HttpStatus.OK) + public List findAllEvents( + @RequestParam(name = "text", required = false) String text, + @RequestParam(name = "categories", required = false) List categories, + @RequestParam(name = "paid", required = false) Boolean paid, + @RequestParam(name = "rangeStart", required = false) String rangeStart, + @RequestParam(name = "rangeEnd", required = false) String rangeEnd, + @RequestParam(name = "onlyAvailable", defaultValue = "true") Boolean onlyAvailable, + @RequestParam(name = "sort", defaultValue = "EVENT_DATE") String sort, + @RequestParam(name = "from", defaultValue = "0") Integer from, + @RequestParam(name = "size", defaultValue = "10") Integer size, + HttpServletRequest request) { + log.info("Пользователь запрвшмвает поиск событий."); + List eventDtos = eventService.findEventsByParametrs( + text, + categories, + paid, + rangeStart, + rangeEnd, + onlyAvailable, + sort, + from, + size + ); + return eventDtos; + } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java index b0386e8..d7903ca 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java @@ -1,10 +1,12 @@ package ru.practicum.evmsevice.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import ru.practicum.evmsevice.model.Event; import java.util.List; -public interface EventRepository extends JpaRepository { +public interface EventRepository extends JpaRepository, + JpaSpecificationExecutor { List findEventsByInitiator_Id(int id); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventSpecification.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventSpecification.java new file mode 100644 index 0000000..b32c4fd --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventSpecification.java @@ -0,0 +1,23 @@ +package ru.practicum.evmsevice.repository; + +import org.springframework.data.jpa.domain.Specification; +import ru.practicum.evmsevice.model.Event; + +import java.util.List; + +public class EventSpecification { + public static Specification annotetionContains(String text) { + return ((root, query, criteriaBuilder) -> + criteriaBuilder.like(root.get("annotation"), "%" + text + "%")); + } + + public static Specification descriptionContains(String text) { + return ((root, query, criteriaBuilder) -> + criteriaBuilder.like(root.get("description"), "%" + text + "%")); + } + + public static Specification categoryIn(List categories) { + return ((root, query, criteriaBuilder) -> + criteriaBuilder.in(root.join("category").get("id")).value(categories)); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java index 6f49208..bcccccb 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java @@ -17,4 +17,13 @@ public interface EventService { EventFullDto adminUpdateEvent(Integer eventId, UpdateEventAdminRequest eventDto); Event findEventById(Integer eventId); + + List findEventsByParametrs(String text, + List categories, + Boolean paid, + String rangeStart, + String rangeEnd, + Boolean onlyAvailable, + String sort, + Integer from, Integer size); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java index b82aeef..a047d59 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java @@ -1,6 +1,7 @@ package ru.practicum.evmsevice.service; import lombok.RequiredArgsConstructor; +import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.practicum.evmsevice.client.StatsClient; @@ -15,6 +16,7 @@ import ru.practicum.evmsevice.model.Event; import ru.practicum.evmsevice.model.User; import ru.practicum.evmsevice.repository.EventRepository; +import ru.practicum.evmsevice.repository.EventSpecification; import ru.practicum.evmsevice.repository.RequestRepository; import java.time.LocalDateTime; @@ -223,4 +225,31 @@ public Event findEventById(Integer eventId) { event.setConfirmedRequests(requestRepository.getCountConfirmedRequestsByEventId(eventId)); return event; } + + /** + * поиск событий + */ + @Override + public List findEventsByParametrs(String text, + List categories, + Boolean paid, + String rangeStart, + String rangeEnd, + Boolean onlyAvailable, + String sort, + Integer from, + Integer size) { + Specification spec = Specification.where(null); + + if (text != null) { + spec = spec.and(EventSpecification.annotetionContains(text)); + spec = spec.or(EventSpecification.descriptionContains(text)); + } + if (categories != null) { + spec = spec.and(EventSpecification.categoryIn(categories)); + } + + List events = eventRepository.findAll(spec); + return events.stream().map(EventMapper::toShortDto).toList(); + } } From 3c236271e2e96044628c2ef21bdaca1796a459c9 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Tue, 10 Jun 2025 23:55:41 +0700 Subject: [PATCH 17/29] =?UTF-8?q?fix:=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../evmsevice/repository/EventRepository.java | 2 + .../repository/EventSpecification.java | 18 ++++++++ .../evmsevice/service/EventServiceImpl.java | 44 ++++++++++++++++++- 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java index d7903ca..bddfd0f 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java @@ -1,5 +1,6 @@ package ru.practicum.evmsevice.repository; +import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import ru.practicum.evmsevice.model.Event; @@ -9,4 +10,5 @@ public interface EventRepository extends JpaRepository, JpaSpecificationExecutor { List findEventsByInitiator_Id(int id); + List findAllOrderByEventDate(Specification specification); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventSpecification.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventSpecification.java index b32c4fd..eb6ccc5 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventSpecification.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventSpecification.java @@ -3,6 +3,9 @@ import org.springframework.data.jpa.domain.Specification; import ru.practicum.evmsevice.model.Event; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Date; import java.util.List; public class EventSpecification { @@ -20,4 +23,19 @@ public static Specification categoryIn(List categories) { return ((root, query, criteriaBuilder) -> criteriaBuilder.in(root.join("category").get("id")).value(categories)); } + + public static Specification paidEqual(Boolean paid) { + return ((root, query, criteriaBuilder) -> + criteriaBuilder.equal(root.get("paid"), paid)); + } + + public static Specification eventDateAfter(LocalDate startDate) { + return ((root, query, criteriaBuilder) -> + criteriaBuilder.greaterThanOrEqualTo(root.get("eventDate"), startDate)); + } + + public static Specification eventDateBefore(LocalDate endDate) { + return ((root, query, criteriaBuilder) -> + criteriaBuilder.lessThan(root.get("eventDate"), endDate)); + } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java index a047d59..e169643 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.jpa.domain.Specification; +import org.springframework.format.datetime.DateFormatter; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.practicum.evmsevice.client.StatsClient; @@ -19,8 +20,12 @@ import ru.practicum.evmsevice.repository.EventSpecification; import ru.practicum.evmsevice.repository.RequestRepository; +import java.time.Instant; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Date; import java.util.List; @Service @@ -28,6 +33,7 @@ @RequiredArgsConstructor public class EventServiceImpl implements EventService{ private static final DateTimeFormatter DATA_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final DateTimeFormatter DATA_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final Integer HOURS_EVENT_DELAY = 2; private final EventRepository eventRepository; @@ -239,17 +245,51 @@ public List findEventsByParametrs(String text, String sort, Integer from, Integer size) { - Specification spec = Specification.where(null); + + LocalDate startDate = null; + LocalDate endDate = null; + try { + if (rangeStart != null && !rangeStart.isEmpty()) { + startDate = LocalDateTime.parse(rangeStart, DATA_TIME_FORMATTER).toLocalDate(); + } + if (rangeEnd != null && !rangeEnd.isEmpty()) { + endDate = LocalDateTime.parse(rangeEnd, DATA_TIME_FORMATTER).toLocalDate(); + } + // если в запросе не указан диапазон дат [rangeStart-rangeEnd], + // то нужно выгружать события, которые произойдут позже текущей даты и времени + if (startDate != null && endDate != null) { + startDate = LocalDate.now(); + } + } catch (DateTimeParseException e) { + throw new ValidationException("Некорректный формат времени. " + e.getMessage()); + } + + Specification spec = Specification.where(null); + // Задаем спецификации для поиска событий + // ...поиск событий по тексту в аннотации и подробном описании события if (text != null) { spec = spec.and(EventSpecification.annotetionContains(text)); spec = spec.or(EventSpecification.descriptionContains(text)); } + // ... поиск по списку идентификаторов категорй if (categories != null) { spec = spec.and(EventSpecification.categoryIn(categories)); } - + // ... поиск платных или бесплатных событий + if (paid != null) { + spec = spec.and(EventSpecification.paidEqual(paid)); + } + // Поиск по date события + /* if (startDate != null) { + spec = spec.and(EventSpecification.eventDateAfter(startDate)); + } + if (endDate != null) { + spec = spec.and(EventSpecification.eventDateBefore(endDate)); + } +*/ List events = eventRepository.findAll(spec); + return events.stream().map(EventMapper::toShortDto).toList(); } } From 43ca45f60e5ebf2e22109ab0941d34e42fb01a0f Mon Sep 17 00:00:00 2001 From: andrej1307 Date: Wed, 11 Jun 2025 15:07:05 +0700 Subject: [PATCH 18/29] =?UTF-8?q?fix:=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9,=20=D0=B4=D0=BE?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20=D0=BF=D0=B0=D1=80?= =?UTF-8?q?=D0=B0=D0=BC=D0=B5=D1=82=D1=80=D0=BE=D0=B2=20=D0=BF=D0=BE=D0=B8?= =?UTF-8?q?=D1=81=D0=BA=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../evmsevice/client/StatsClient.java | 6 +++ .../evmsevice/dto/EventShortDto.java | 2 + .../model/EventConfirmedRequestCount.java | 15 ++++++ .../repository/EventSpecification.java | 4 +- .../repository/RequestRepository.java | 6 +++ .../evmsevice/service/EventServiceImpl.java | 47 ++++++++++++++----- .../src/main/resources/application.properties | 14 +++--- ewm-service/src/main/resources/schema.sql | 2 +- 8 files changed, 75 insertions(+), 21 deletions(-) create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/model/EventConfirmedRequestCount.java diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java b/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java index e0ce084..c7a05bf 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java @@ -62,4 +62,10 @@ public Integer getEventViews(Integer eventId, Boolean unique) { } return dtos.get(0).getHits(); } + + public List getEventViewsByUris(List eventUris, Boolean unique) { + Map parameters = Map.of("uris", eventUris, + "unique", unique); + return getStatsList(PREFIX_STATS, parameters); + } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventShortDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventShortDto.java index 92a02d6..b0e2694 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventShortDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventShortDto.java @@ -1,5 +1,6 @@ package ru.practicum.evmsevice.dto; +import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -16,6 +17,7 @@ public class EventShortDto { private String annotation; private CategoryDto category; private Integer confirmedRequest; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime eventDate; private UserShortDto initiator; private Boolean paid; diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/EventConfirmedRequestCount.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/EventConfirmedRequestCount.java new file mode 100644 index 0000000..7c4405d --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/EventConfirmedRequestCount.java @@ -0,0 +1,15 @@ +package ru.practicum.evmsevice.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class EventConfirmedRequestCount { + private Integer eventId; + private Long confirmedRequestCount; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventSpecification.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventSpecification.java index eb6ccc5..7de9fff 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventSpecification.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventSpecification.java @@ -29,12 +29,12 @@ public static Specification paidEqual(Boolean paid) { criteriaBuilder.equal(root.get("paid"), paid)); } - public static Specification eventDateAfter(LocalDate startDate) { + public static Specification eventDateAfter(LocalDateTime startDate) { return ((root, query, criteriaBuilder) -> criteriaBuilder.greaterThanOrEqualTo(root.get("eventDate"), startDate)); } - public static Specification eventDateBefore(LocalDate endDate) { + public static Specification eventDateBefore(LocalDateTime endDate) { return ((root, query, criteriaBuilder) -> criteriaBuilder.lessThan(root.get("eventDate"), endDate)); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java index c17296b..a51f49f 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java @@ -4,6 +4,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import ru.practicum.evmsevice.enums.RequestStatus; +import ru.practicum.evmsevice.model.EventConfirmedRequestCount; import ru.practicum.evmsevice.model.Request; import java.util.List; @@ -15,4 +16,9 @@ public interface RequestRepository extends JpaRepository { @Query("SELECT COUNT(r) FROM Request r WHERE r.event.id = :eventId AND r.status = 'CONFIRMED'") Integer getCountConfirmedRequestsByEventId(@Param("eventId") int eventId); + + @Query("SELECT new ru.practicum.evmsevice.model.EventConfirmedRequestCount(r.event.id, COUNT(r)) " + + "FROM Request r WHERE r. event.id IN (:ids) AND r.status = 'CONFIRMED'" + + "GROUP BY r.event.id") + List getCountConfirmedRequests(@Param("ids") List ids); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java index e169643..baef182 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java @@ -1,6 +1,7 @@ package ru.practicum.evmsevice.service; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; import org.springframework.format.datetime.DateFormatter; import org.springframework.stereotype.Service; @@ -15,18 +16,19 @@ import ru.practicum.evmsevice.mapper.EventMapper; import ru.practicum.evmsevice.model.Category; import ru.practicum.evmsevice.model.Event; +import ru.practicum.evmsevice.model.EventConfirmedRequestCount; import ru.practicum.evmsevice.model.User; import ru.practicum.evmsevice.repository.EventRepository; import ru.practicum.evmsevice.repository.EventSpecification; import ru.practicum.evmsevice.repository.RequestRepository; +import ru.practicum.statdto.StatsDto; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; -import java.util.Date; -import java.util.List; +import java.util.*; @Service @Transactional @@ -247,19 +249,19 @@ public List findEventsByParametrs(String text, Integer size) { - LocalDate startDate = null; - LocalDate endDate = null; + LocalDateTime startDate = null; + LocalDateTime endDate = null; try { if (rangeStart != null && !rangeStart.isEmpty()) { - startDate = LocalDateTime.parse(rangeStart, DATA_TIME_FORMATTER).toLocalDate(); + startDate = LocalDateTime.parse(rangeStart, DATA_TIME_FORMATTER); } if (rangeEnd != null && !rangeEnd.isEmpty()) { - endDate = LocalDateTime.parse(rangeEnd, DATA_TIME_FORMATTER).toLocalDate(); + endDate = LocalDateTime.parse(rangeEnd, DATA_TIME_FORMATTER); } // если в запросе не указан диапазон дат [rangeStart-rangeEnd], // то нужно выгружать события, которые произойдут позже текущей даты и времени if (startDate != null && endDate != null) { - startDate = LocalDate.now(); + startDate = LocalDateTime.now(); } } catch (DateTimeParseException e) { throw new ValidationException("Некорректный формат времени. " + e.getMessage()); @@ -281,15 +283,38 @@ public List findEventsByParametrs(String text, spec = spec.and(EventSpecification.paidEqual(paid)); } // Поиск по date события - /* if (startDate != null) { + if (startDate != null) { spec = spec.and(EventSpecification.eventDateAfter(startDate)); } if (endDate != null) { spec = spec.and(EventSpecification.eventDateBefore(endDate)); } -*/ - List events = eventRepository.findAll(spec); - return events.stream().map(EventMapper::toShortDto).toList(); + List events = eventRepository.findAll(spec, + Sort.by("eventDate").descending()); + + Map eventMap = new HashMap(); + List eventUris = new ArrayList<>(); + for (Event event : events) { + eventMap.put(event.getId(), EventMapper.toShortDto(event)); + eventUris.add(String.format("/events/%d", event.getId())); + } + + // заполняем количество заявок + List counts = + requestRepository.getCountConfirmedRequests(eventMap.keySet().stream().toList()); + for(EventConfirmedRequestCount count : counts) { + Integer eventId = count.getEventId(); + eventMap.get(eventId).setConfirmedRequest(count.getConfirmedRequestCount().intValue()); + } + + // заполняем количество просмотров + List statsDtos = statsClient.getEventViewsByUris(eventUris, true); + for (StatsDto dto : statsDtos) { + Integer eventId = Integer.parseInt(dto.getUri().split("/")[1]); + eventMap.get(eventId).setViews(dto.getHits()); + } + + return eventMap.values().stream().toList(); } } diff --git a/ewm-service/src/main/resources/application.properties b/ewm-service/src/main/resources/application.properties index 7bb57dd..7b48cb2 100644 --- a/ewm-service/src/main/resources/application.properties +++ b/ewm-service/src/main/resources/application.properties @@ -3,14 +3,14 @@ spring.application.name=ewm-service spring.sql.init.mode=always spring.jackson.date-format=yyyy-MM-dd HH:mm:ss -spring.datasource.driverClassName=org.postgresql.Driver -spring.datasource.url=jdbc:postgresql://192.168.0.102:5434/ewmdb -spring.datasource.username=ewmdb -spring.datasource.password=ewmdb - -#spring.datasource.driverClassName=org.h2.Driver -#spring.datasource.url=jdbc:h2:mem:ewmdb +#spring.datasource.driverClassName=org.postgresql.Driver +#spring.datasource.url=jdbc:postgresql://192.168.0.102:5434/ewmdb #spring.datasource.username=ewmdb #spring.datasource.password=ewmdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.url=jdbc:h2:file:C:/dev/data/ewmdb +spring.datasource.username=ewmdb +spring.datasource.password=ewmdb + statserver.url=http://localhost:9090 \ No newline at end of file diff --git a/ewm-service/src/main/resources/schema.sql b/ewm-service/src/main/resources/schema.sql index b0d95bd..f2538e3 100644 --- a/ewm-service/src/main/resources/schema.sql +++ b/ewm-service/src/main/resources/schema.sql @@ -31,7 +31,7 @@ CREATE TABLE IF NOT EXISTS events ( state VARCHAR(32), title VARCHAR(128), CONSTRAINT pk_event PRIMARY KEY (id), - CONSTRAINT fk_events_to_users FOREIGN KEY (user_id) REFERENCES users (id), + CONSTRAINT fk_events_to_users FOREIGN KEY (initiator_id) REFERENCES users (id), CONSTRAINT fk_events_to_categories FOREIGN KEY (category_id) REFERENCES categories (id) ); From ad7ccdefe44a465e1c9b32404d0abfeaba9bbffa Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Thu, 12 Jun 2025 19:22:58 +0700 Subject: [PATCH 19/29] =?UTF-8?q?feat:=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9=20=D0=B0=D0=B4=D0=BC?= =?UTF-8?q?=D0=B8=D0=BD=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../evmsevice/client/StatsClient.java | 9 +- .../evmsevice/controller/AdminController.java | 16 +++ .../controller/PublicController.java | 2 +- .../evmsevice/dto/EventShortDto.java | 4 + .../evmsevice/mapper/EventMapper.java | 17 ++- .../ru/practicum/evmsevice/model/Event.java | 2 - .../evmsevice/repository/EventRepository.java | 1 - .../repository/EventSpecification.java | 11 ++ .../evmsevice/service/EventService.java | 9 ++ .../evmsevice/service/EventServiceImpl.java | 111 +++++++++++++++++- .../evmsevice/service/RequestServiceImpl.java | 24 ++-- .../src/main/resources/application.properties | 14 +-- .../statsvc/repository/StatDbStorage.java | 2 +- 13 files changed, 182 insertions(+), 40 deletions(-) diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java b/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java index c7a05bf..f40fc95 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java @@ -64,8 +64,13 @@ public Integer getEventViews(Integer eventId, Boolean unique) { } public List getEventViewsByUris(List eventUris, Boolean unique) { - Map parameters = Map.of("uris", eventUris, + StringBuilder urisBuilder = new StringBuilder(eventUris.get(0)); + for (int i = 1; i < eventUris.size(); i++) { + urisBuilder.append(",").append(eventUris.get(i)); + } + Map parameters = Map.of("uris", urisBuilder.toString(), "unique", unique); - return getStatsList(PREFIX_STATS, parameters); + List dtos = getStatsList(PREFIX_STATS, parameters); + return dtos; } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java index 1c84ba5..955e6d7 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java @@ -102,6 +102,22 @@ public void deletecategory(@PathVariable int id, HttpServletRequest request) { categoryService.deleteCategory(id); } + @GetMapping("/events") + @ResponseStatus(HttpStatus.OK) + public List findEvents( + @RequestParam(name = "users", required = false) List users, + @RequestParam(name = "states", required = false) List states, + @RequestParam(name = "categories", required = false) List categories, + @RequestParam(name = "rangeStart", required = false) String rangeStart, + @RequestParam(name = "rangeEnd", required = false) String rangeEnd, + @RequestParam(name = "from", defaultValue = "0") Integer from, + @RequestParam(name = "size", defaultValue = "10") Integer size, + HttpServletRequest request) { + log.info("Администратор запрашивает список событий. users:{}, states:{}, categories:{} rangeStart:{}, rangeStart:{}.", + users, states, categories, rangeStart, rangeEnd); + return eventService.findEventsByAdmin(states, users, categories, rangeStart, rangeEnd, from, size ); + } + @PatchMapping("/events/{eventId}") @ResponseStatus(HttpStatus.OK) public EventFullDto updateEvent(@PathVariable Integer eventId, diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java index 68ea5bf..7cb1efe 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java @@ -55,7 +55,7 @@ public List findAllEvents( @RequestParam(name = "paid", required = false) Boolean paid, @RequestParam(name = "rangeStart", required = false) String rangeStart, @RequestParam(name = "rangeEnd", required = false) String rangeEnd, - @RequestParam(name = "onlyAvailable", defaultValue = "true") Boolean onlyAvailable, + @RequestParam(name = "onlyAvailable", defaultValue = "false") Boolean onlyAvailable, @RequestParam(name = "sort", defaultValue = "EVENT_DATE") String sort, @RequestParam(name = "from", defaultValue = "0") Integer from, @RequestParam(name = "size", defaultValue = "10") Integer size, diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventShortDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventShortDto.java index b0e2694..9d1b327 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventShortDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventShortDto.java @@ -1,6 +1,7 @@ package ru.practicum.evmsevice.dto; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -23,4 +24,7 @@ public class EventShortDto { private Boolean paid; private String title; private Integer views; + @JsonIgnore + private Integer participantLimit; + } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java index 19f9e7c..daae687 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java @@ -47,28 +47,35 @@ public static EventFullDto toFullDto(Event event) { dto.setCategory(CategoryMapper.toDto(event.getCategory())); dto.setInitiator(UserMapper.toUserShortDto(event.getInitiator())); dto.setLocation(new Location(event.getLat(), event.getLon())); - dto.setParticipantLimit(event.getParticipantLimit()); - dto.setConfirmedRequests(0); - if(event.getConfirmedRequests() != null) { - dto.setConfirmedRequests(event.getConfirmedRequests()); + dto.setParticipantLimit(0); + if(event.getParticipantLimit() != null) { + dto.setParticipantLimit(event.getParticipantLimit()); } dto.setRequestModeration(event.getRequestModeration()); dto.setState(event.getState()); dto.setPaid(event.getPaid()); dto.setPublishedOn(event.getPublishedOn()); dto.setTitle(event.getTitle()); + dto.setConfirmedRequests(0); + dto.setViews(0); return dto; } public static EventShortDto toShortDto(Event event) { EventShortDto dto = new EventShortDto(); dto.setId(event.getId()); + dto.setTitle(event.getTitle()); dto.setAnnotation(event.getAnnotation()); dto.setCategory(CategoryMapper.toDto(event.getCategory())); dto.setInitiator(UserMapper.toUserShortDto(event.getInitiator())); dto.setEventDate(event.getEventDate()); dto.setPaid(event.getPaid()); - dto.setTitle(event.getTitle()); + dto.setParticipantLimit(0); + if(event.getParticipantLimit() != null) { + dto.setParticipantLimit(event.getParticipantLimit()); + } + dto.setConfirmedRequest(0); + dto.setViews(0); return dto; } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java index bf549f9..138dabb 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java @@ -22,8 +22,6 @@ public class Event { @ManyToOne @JoinColumn(name = "category_id") private Category category; - @Column(name = "confirmedrequests") - private Integer confirmedRequests; @Column(name = "createdon", nullable = false) private LocalDateTime createdOn; @Column(name = "description") diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java index bddfd0f..380e3fa 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java @@ -10,5 +10,4 @@ public interface EventRepository extends JpaRepository, JpaSpecificationExecutor { List findEventsByInitiator_Id(int id); - List findAllOrderByEventDate(Specification specification); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventSpecification.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventSpecification.java index 7de9fff..83134d6 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventSpecification.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventSpecification.java @@ -1,6 +1,7 @@ package ru.practicum.evmsevice.repository; import org.springframework.data.jpa.domain.Specification; +import ru.practicum.evmsevice.enums.EventState; import ru.practicum.evmsevice.model.Event; import java.time.LocalDate; @@ -38,4 +39,14 @@ public static Specification eventDateBefore(LocalDateTime endDate) { return ((root, query, criteriaBuilder) -> criteriaBuilder.lessThan(root.get("eventDate"), endDate)); } + + public static Specification eventInitiatorIdIn(List initiatorIds) { + return ((root, query, criteriaBuilder) -> + criteriaBuilder.in(root.join("initiator").get("id")).value(initiatorIds)); + } + + public static Specification eventStateIn(List states) { + return ((root, query, criteriaBuilder) -> + criteriaBuilder.in(root.get("state")).value(states)); + } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java index bcccccb..d1ee1be 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java @@ -1,5 +1,6 @@ package ru.practicum.evmsevice.service; +import org.springframework.web.bind.annotation.RequestParam; import ru.practicum.evmsevice.dto.*; import ru.practicum.evmsevice.model.Event; @@ -26,4 +27,12 @@ List findEventsByParametrs(String text, Boolean onlyAvailable, String sort, Integer from, Integer size); + + List findEventsByAdmin(List states, + List users, + List categories, + String rangeStart, + String rangeEnd, + Integer from, + Integer size); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java index baef182..7d31c87 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java @@ -75,8 +75,8 @@ public EventFullDto getEventById(Integer eventId, Integer userId) { throw new ValidationException("Пользователь id=" + userId + " не является инициатором события id=" + eventId); } - event.setConfirmedRequests(requestRepository.getCountConfirmedRequestsByEventId(eventId)); EventFullDto eventFullDto = EventMapper.toFullDto(event); + eventFullDto.setConfirmedRequests(requestRepository.getCountConfirmedRequestsByEventId(eventId)); eventFullDto.setViews(statsClient.getEventViews(eventId, true)); return eventFullDto; } @@ -230,12 +230,11 @@ public Event findEventById(Integer eventId) { Event event = eventRepository.findById(eventId) .orElseThrow(() -> new NotFoundException("Не найдено событие id=" + eventId)); - event.setConfirmedRequests(requestRepository.getCountConfirmedRequestsByEventId(eventId)); return event; } /** - * поиск событий + * поиск событий пользователем */ @Override public List findEventsByParametrs(String text, @@ -293,7 +292,7 @@ public List findEventsByParametrs(String text, List events = eventRepository.findAll(spec, Sort.by("eventDate").descending()); - Map eventMap = new HashMap(); + TreeMap eventMap = new TreeMap(); List eventUris = new ArrayList<>(); for (Event event : events) { eventMap.put(event.getId(), EventMapper.toShortDto(event)); @@ -311,10 +310,110 @@ public List findEventsByParametrs(String text, // заполняем количество просмотров List statsDtos = statsClient.getEventViewsByUris(eventUris, true); for (StatsDto dto : statsDtos) { - Integer eventId = Integer.parseInt(dto.getUri().split("/")[1]); + Integer eventId = Integer.parseInt(dto.getUri().split("/")[2]); eventMap.get(eventId).setViews(dto.getHits()); } - return eventMap.values().stream().toList(); + List eventDtos = new ArrayList<>(); + if(onlyAvailable) { + // Фильтруем события у котрыз не исчерпано количество заявок + eventDtos = eventMap.values() + .stream() + .filter(eventDto -> eventDto.getParticipantLimit() != 0 + && eventDto.getConfirmedRequest() < eventDto.getParticipantLimit()) + .toList(); + } else { + eventDtos.addAll(eventMap.values()); + } + + if(sort.equalsIgnoreCase("VIEWS")) { + return eventDtos.stream() + .sorted(Comparator.comparing(EventShortDto::getViews)) + .skip(from).limit(size).toList(); + } + return eventMap.values().stream().skip(from).limit(size).toList(); + } + + /** + * Поиск событий администратором + */ + @Override + public List findEventsByAdmin(List states, + List users, + List categories, + String rangeStart, + String rangeEnd, + Integer from, + Integer size) { + LocalDateTime startDate = null; + LocalDateTime endDate = null; + try { + if (rangeStart != null && !rangeStart.isEmpty()) { + startDate = LocalDateTime.parse(rangeStart, DATA_TIME_FORMATTER); + } + if (rangeEnd != null && !rangeEnd.isEmpty()) { + endDate = LocalDateTime.parse(rangeEnd, DATA_TIME_FORMATTER); + } + if (startDate != null && endDate != null) { + startDate = LocalDateTime.now(); + } + } catch (DateTimeParseException e) { + throw new ValidationException("Некорректный формат времени. " + e.getMessage()); + } + + Specification spec = Specification.where(null); + // Задаем спецификации для поиска событий + // ...поиск событий по списку идентификаторов инициаторов + if (users != null) { + spec = spec.and(EventSpecification.eventInitiatorIdIn(users)); + } + // ... поиск по списку идентификаторов категорй + if (categories != null) { + spec = spec.and(EventSpecification.categoryIn(categories)); + } + // ... поиск по списку состояний + // List enumStates = states.stream().map(state -> EventState.valueOf(state)).toList(); + if (states != null) { + spec = spec.and(EventSpecification.eventStateIn(states)); + } + // Поиск по date события + if (startDate != null) { + spec = spec.and(EventSpecification.eventDateAfter(startDate)); + } + if (endDate != null) { + spec = spec.and(EventSpecification.eventDateBefore(endDate)); + } + + List events = eventRepository.findAll(spec, + Sort.by("eventDate").descending()); + if (events.isEmpty()) { + return List.of(); + } + + TreeMap eventMap = new TreeMap(); + List eventUris = new ArrayList<>(); + for (Event event : events) { + eventMap.put(event.getId(), EventMapper.toFullDto(event)); + eventUris.add(String.format("/events/%d", event.getId())); + } + + // заполняем количество заявок + List counts = + requestRepository.getCountConfirmedRequests(eventMap.keySet().stream().toList()); + for(EventConfirmedRequestCount count : counts) { + Integer eventId = count.getEventId(); + eventMap.get(eventId).setConfirmedRequests(count.getConfirmedRequestCount().intValue()); + } + + // заполняем количество просмотров + List statsDtos = statsClient.getEventViewsByUris(eventUris, true); + for (StatsDto dto : statsDtos) { + Integer eventId = Integer.parseInt(dto.getUri().split("/")[2]); + eventMap.get(eventId).setViews(dto.getHits()); + } + + List eventDtos = new ArrayList<>(); + eventDtos.addAll(eventMap.values()); + return eventDtos.stream().skip(from).limit(size).toList(); } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java index 343fdc3..728ad7b 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java @@ -1,7 +1,6 @@ package ru.practicum.evmsevice.service; import lombok.RequiredArgsConstructor; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.practicum.evmsevice.dto.RequestGroupDto; @@ -46,7 +45,7 @@ public Request createRequest(Integer userId, Integer eventId) { "Value: " + event.getState() ); } - Integer confirmedRequests = event.getConfirmedRequests(); + Integer confirmedRequests = requestRepository.getCountConfirmedRequestsByEventId(eventId); if (confirmedRequests != null) { if (confirmedRequests.equals(event.getParticipantLimit())) { throw new ValidationException( @@ -63,7 +62,6 @@ public Request createRequest(Integer userId, Integer eventId) { request.setStatus(RequestStatus.PENDING); if (!event.getRequestModeration()) { request.setStatus(RequestStatus.CONFIRMED); - event.setConfirmedRequests(confirmedRequests + 1); } request.setCreated(LocalDateTime.now()); return requestRepository.save(request); @@ -115,19 +113,20 @@ public RequestGroupDto updateRequestsStatus(Integer userId, Integer eventId, Req if (!event.getInitiator().getId().equals(userId)) { throw new ValidationException( "Field: event.initiator_id. Error: " + - "Пользователь id=" + userId + " не является инициатором события id=" + eventId + - ". Value: " + event.getInitiator().getId() + "Пользователь id=" + userId + " не является инициатором события id=" + eventId + + ". Value: " + event.getInitiator().getId() ); } // ...нельзя подтвердить заявку, если уже достигнут лимит по заявкам на данное событие // (Ожидается код ошибки 409) + Integer confirmedRequests = requestRepository.getCountConfirmedRequestsByEventId(eventId); if ((event.getParticipantLimit() > 0) - && event.getParticipantLimit().equals(event.getConfirmedRequests())){ + && event.getParticipantLimit().equals(confirmedRequests)) { throw new DataConflictException( - "Field: event.confirmedRequests. " + - "Error: Достигнуто максимальное количество заявок для события id=" + eventId + - ". Value: " + event.getConfirmedRequests() + "Field: event.confirmedRequests. " + + "Error: Достигнуто максимальное количество заявок для события id=" + eventId + + ". Value: " + confirmedRequests ); } @@ -139,15 +138,10 @@ public RequestGroupDto updateRequestsStatus(Integer userId, Integer eventId, Req Collections.sort(requestIds); RequestStatus status = requestUpdateDto.getStatus(); - Integer confirmedRequests = 0; - if (event.getConfirmedRequests() != null) { - confirmedRequests = event.getConfirmedRequests(); - } - // Проверяем заявки из списка for (Integer requestId : requestIds) { Request request = requestRepository.findById(requestId) - .orElseThrow(() -> new NotFoundException("Не наайдена заявка id=" + requestId)); + .orElseThrow(() -> new NotFoundException("Не найдена заявка id=" + requestId)); // ... статус можно изменить только у заявок, находящихся в состоянии ожидания // (Ожидается код ошибки 409) if (!request.getStatus().equals(RequestStatus.PENDING)) { diff --git a/ewm-service/src/main/resources/application.properties b/ewm-service/src/main/resources/application.properties index 7b48cb2..ce58e1d 100644 --- a/ewm-service/src/main/resources/application.properties +++ b/ewm-service/src/main/resources/application.properties @@ -3,14 +3,14 @@ spring.application.name=ewm-service spring.sql.init.mode=always spring.jackson.date-format=yyyy-MM-dd HH:mm:ss -#spring.datasource.driverClassName=org.postgresql.Driver -#spring.datasource.url=jdbc:postgresql://192.168.0.102:5434/ewmdb -#spring.datasource.username=ewmdb -#spring.datasource.password=ewmdb - -spring.datasource.driverClassName=org.h2.Driver -spring.datasource.url=jdbc:h2:file:C:/dev/data/ewmdb +spring.datasource.driverClassName=org.postgresql.Driver +spring.datasource.url=jdbc:postgresql://192.168.0.102:5434/ewmdb spring.datasource.username=ewmdb spring.datasource.password=ewmdb +#spring.datasource.driverClassName=org.h2.Driver +#spring.datasource.url=jdbc:h2:file:C:/dev/data/ewmdb +#spring.datasource.username=ewmdb +#spring.datasource.password=ewmdb + statserver.url=http://localhost:9090 \ No newline at end of file diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatDbStorage.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatDbStorage.java index e6ff0be..6e45900 100644 --- a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatDbStorage.java +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatDbStorage.java @@ -58,7 +58,7 @@ public List getViewStats(LocalDateTime start, LocalDateTime end, List StringBuilder sql = new StringBuilder(); sql.append("SELECT app, uri, count(ip) as hits FROM"); if (unique) { - sql.append(" (SELECT DISTINCT ON (ip) app, uri, ip, timestamp FROM endpointhits)"); + sql.append(" (SELECT DISTINCT ON (ip, uri) app, uri, ip, timestamp FROM endpointhits)"); } else { sql.append(" endpointhits"); } From ac8f7a0dacb4ee46b6ce45361b05fdf022629d26 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Tue, 17 Jun 2025 00:01:32 +0700 Subject: [PATCH 20/29] =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D1=8B=20PostMan?= =?UTF-8?q?=20-=20OK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Postman/ewm-main-service.json | 14630 ++++++++++++++++ Postman/ewm-stat-service.json | 1027 ++ .../explore-with-me.postman_collection.json | 540 - Postman/objects.json | 56 - docker-compose.yml | 3 + .../evmsevice/client/StatsClient.java | 8 +- .../evmsevice/controller/AdminController.java | 48 +- .../evmsevice/controller/ErrorAdvisor.java | 25 +- .../controller/PublicController.java | 92 +- .../controller/TestClientController.java | 4 +- .../evmsevice/controller/UserController.java | 24 +- .../evmsevice/dto/CompilationDto.java | 21 + .../evmsevice/dto/EventShortDto.java | 2 +- .../evmsevice/dto/NewCompilationDto.java | 25 + .../practicum/evmsevice/dto/NewEventDto.java | 12 +- .../evmsevice/dto/PatchCompilationDto.java | 24 + .../dto/UpdateEventAdminRequest.java | 26 +- .../evmsevice/dto/UpdateEventUserRequest.java | 27 +- .../ru/practicum/evmsevice/dto/UserDto.java | 3 + .../exception/BadRequestException.java | 7 + .../evmsevice/mapper/CompilationMapper.java | 36 + .../evmsevice/mapper/EventMapper.java | 18 +- .../evmsevice/model/Compilation.java | 32 + .../ru/practicum/evmsevice/model/Event.java | 7 + .../repository/CompilationRepository.java | 10 + .../evmsevice/repository/EventRepository.java | 2 + .../evmsevice/repository/UserRepository.java | 2 + .../evmsevice/service/CategoryService.java | 7 + .../service/CategoryServiceImpl.java | 7 + .../evmsevice/service/CompilationService.java | 19 + .../service/CompilationServiceImpl.java | 85 + .../evmsevice/service/EventService.java | 2 + .../evmsevice/service/EventServiceImpl.java | 259 +- .../evmsevice/service/RequestService.java | 5 +- .../evmsevice/service/RequestServiceImpl.java | 29 +- .../evmsevice/service/UserService.java | 2 + .../evmsevice/service/UserServiceImpl.java | 7 + .../src/main/resources/application.properties | 7 +- ewm-service/src/main/resources/schema.sql | 24 +- .../ru/practicum/statclient/BaseClient.java | 11 +- .../ru/practicum/statdto/StatsDtoList.java | 14 - .../statsvc/controller/StatController.java | 9 +- .../ru/practicum/statsvc/model/ViewStats.java | 6 +- .../statsvc/repository/StatDbStorage.java | 8 +- .../statsvc/repository/StatStorage.java | 2 +- .../statsvc/service/StatServiceImpl.java | 8 + .../src/main/resources/application.properties | 6 +- .../stat-svc/src/main/resources/schema.sql | 2 + 48 files changed, 16399 insertions(+), 831 deletions(-) create mode 100644 Postman/ewm-main-service.json create mode 100644 Postman/ewm-stat-service.json delete mode 100644 Postman/explore-with-me.postman_collection.json delete mode 100644 Postman/objects.json create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/dto/CompilationDto.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewCompilationDto.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/dto/PatchCompilationDto.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/exception/BadRequestException.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CompilationMapper.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/model/Compilation.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/repository/CompilationRepository.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/service/CompilationService.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/service/CompilationServiceImpl.java delete mode 100644 stats-server/stat-dto/src/main/java/ru/practicum/statdto/StatsDtoList.java diff --git a/Postman/ewm-main-service.json b/Postman/ewm-main-service.json new file mode 100644 index 0000000..2ab4c3e --- /dev/null +++ b/Postman/ewm-main-service.json @@ -0,0 +1,14630 @@ +{ + "info": { + "_postman_id": "4f622f31-328a-4506-95bd-66359cfbe749", + "name": "Test Explore With Me - Main service", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "23073145", + "_collection_link": "https://universal-shadow-295426.postman.co/workspace/My-Workspace~4200f6aa-0504-44b1-8a1d-707d0dcbd5ce/collection/13708500-4f622f31-328a-4506-95bd-66359cfbe749?action=share&source=collection_link&creator=23073145" + }, + "item": [ + { + "name": "Validation", + "item": [ + { + "name": "Event", + "item": [ + { + "name": "Required query params", + "item": [ + { + "name": "Добавление запроса от текущего пользователя на участие в событии без обязательного query params", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " pm.request.removeQueryParams(['eventId']);\r", + " pm.collectionVariables.set('uid', submittedUser.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/requests", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "requests" + ], + "query": [ + { + "key": "eventId", + "value": "0", + "description": "(Required) id события", + "disabled": true + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- нельзя добавить повторный запрос\n- инициатор события не может добавить запрос на участие в своём событии\n- нельзя участвовать в неопубликованном событии\n- если у события достигнут лимит запросов на участие - необходимо вернуть ошибку\n- если для события отключена пре-модерация запросов на участие, то запрос должен автоматически перейти в состояние подтвержденного" + }, + "response": [] + } + ] + }, + { + "name": "Unrequired query params", + "item": [ + { + "name": "Получение событий, добавленных текущим пользователем без нескольких Query params", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "query": [ + { + "key": "from", + "value": "0", + "description": "количество элементов, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "1000", + "description": "количество элементов в наборе", + "disabled": true + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + } + }, + "response": [] + }, + { + "name": "Получение событий с возможностью фильтрации без нескольких Query params", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " pm.request.removeQueryParams(['text', 'categories', 'paid']);\r", + " pm.request.addQueryParams([`text=` + event.annotation, 'categories=' + category.id, 'paid=' + event.paid]);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "events" + ], + "query": [ + { + "key": "text", + "value": "0", + "description": "текст для поиска в содержимом аннотации и подробном описании события", + "disabled": true + }, + { + "key": "categories", + "value": "0", + "description": "список идентификаторов категорий в которых будет вестись поиск", + "disabled": true + }, + { + "key": "paid", + "value": "true", + "description": "поиск только платных/бесплатных событий", + "disabled": true + }, + { + "key": "rangeStart", + "value": "2022-01-06%2013%3A30%3A38", + "description": "дата и время не раньше которых должно произойти событие", + "disabled": true + }, + { + "key": "rangeEnd", + "value": "2097-09-06%2013%3A30%3A38", + "description": "дата и время не позже которых должно произойти событие", + "disabled": true + }, + { + "key": "onlyAvailable", + "value": "false", + "description": "только события у которых не исчерпан лимит запросов на участие", + "disabled": true + }, + { + "key": "sort", + "value": "EVENT_DATE", + "description": "Вариант сортировки: по дате события или по количеству просмотров", + "disabled": true + }, + { + "key": "from", + "value": "0", + "description": "количество событий, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "1000", + "description": "количество событий в наборе", + "disabled": true + } + ] + }, + "description": "Обратите внимание: \n- это публичный эндпоинт, соответственно в выдаче должны быть только опубликованные события\n- текстовый поиск (по аннотации и подробному описанию) должен быть без учета регистра букв\n- если в запросе не указан диапазон дат [rangeStart-rangeEnd], то нужно выгружать события, которые произойдут позже текущей даты и времени\n- информация о каждом событии должна включать в себя количество просмотров и количество уже одобренных заявок на участие\n- информацию о том, что по этому эндпоинту был осуществлен и обработан запрос, нужно сохранить в сервисе статистики" + }, + "response": [] + } + ] + }, + { + "name": "Required params in body", + "item": [ + { + "name": "Добавление события без поля description", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " delete event[\"description\"];\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление события с пустым описанием", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event[\"description\"] = '';\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление события со строкой из пробелов в качестве описания", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event[\"description\"] = ' ';\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление события без поля annotation", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " delete event[\"annotation\"];\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление события с пустой аннотацией", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event[\"annotation\"] = '';\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление события со строкой из пробелов в качестве аннотации", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event[\"annotation\"] = ' ';\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление события с отрицательным лимитом участников", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event[\"participantLimit\"] = -6;\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Изменение события добавленного текущим пользователем. Изменение лимита участников на отрицательное значение", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.collectionVariables.set(\"response\", event);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " participantLimit : -156\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.have.status(400);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + } + ] + }, + { + "name": "Misc tests", + "item": [ + { + "name": "Отклонение публикации события", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let new_event, event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " await pm.sendRequest({\r", + " url : \"http://localhost:8080/admin/events/\" + event.id,\r", + " method : \"PATCH\",\r", + " header: { \"Content-Type\": \"application/json\" },\r", + " body: JSON.stringify({\r", + " stateAction: \"REJECT_EVENT\"\r", + " })\r", + " }, (error, response) => {\r", + "\r", + " });\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.collectionVariables.set(\"response\", event);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " stateAction: \"SEND_TO_REVIEW\"\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get(\"response\");\r", + "const target = pm.response.json();\r", + "\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, description, participantLimit, state, createdOn, location, requestModeration\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('annotation');\r", + "pm.expect(target).to.have.property('category');\r", + "pm.expect(target).to.have.property('paid');\r", + "pm.expect(target).to.have.property('eventDate');\r", + "pm.expect(target).to.have.property('initiator');\r", + "pm.expect(target).to.have.property('description');\r", + "pm.expect(target).to.have.property('participantLimit');\r", + "pm.expect(target).to.have.property('state');\r", + "pm.expect(target).to.have.property('createdOn');\r", + "pm.expect(target).to.have.property('location');\r", + "pm.expect(target).to.have.property('requestModeration');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.annotation).equal(target.annotation, 'Аннотация отменённого события должна соответствовать аннотации события до отмены');\r", + " pm.expect(source.category.id).equal(target.category.id, 'Категория отменённого события должна соответствовать категории события до отмены');\r", + " pm.expect(source.paid.toString()).equal(target.paid.toString(), 'Стоимость отменённого события должна соответствовать стоимости события до отмены');\r", + " pm.expect(source.eventDate).equal(target.eventDate, 'Дата проведения отменённого события должна соответствовать дате проведения события до отмены');\r", + " pm.expect(source.description).equal(target.description, 'Описание отменённого события должно соответствовать описанию события до отмены');\r", + " pm.expect(source.title).equal(target.title, 'Название отменённого события должно соответствовать названию события до отмены');\r", + " pm.expect(source.participantLimit.toString()).equal(target.participantLimit.toString(), 'Лимит участников отменённого события должен соответствовать лимиту участников события до отмены');\r", + " pm.expect(source.paid.toString()).equal(target.paid.toString(), 'Стоимость отменённого события должна соответствовать стоимости события до отмены');\r", + "});\r", + "\r", + "pm.test(\"Событие должно иметь статус CANCELED при возвращении от администратора и статус PENDING после выполнения запроса\", function () {\r", + " pm.expect(target.state).equal(\"PENDING\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Попытка получения информации о событии по публичному эндпоинту без публикации", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.collectionVariables.set('response', event);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 404 и данные в формате json\", function () {\r", + " pm.response.to.be.notFound; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/events/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "events", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n- событие должно быть опубликовано\n- информация о событии должна включать в себя количество просмотров и количество подтвержденных запросов\n- информацию о том, что по этому эндпоинту был осуществлен и обработан запрос, нужно сохранить в сервисе статистики" + }, + "response": [] + }, + { + "name": "Поиск событий с проверкой параметров", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const user2 = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = rnd.getEvent(category.id)\r", + " event.requestModeration = true;\r", + " event.participantLimit = 2;\r", + " event = await api.addEvent(user.id, event);\r", + " event = await api.publishEvent(event.id);\r", + " pm.request.removeQueryParams(['users', 'categories']);\r", + " pm.request.addQueryParams([`users=` + user.id, 'categories=' + category.id]);\r", + " pm.collectionVariables.set('response', event);\r", + " await pm.sendRequest({\r", + " url : \"http://localhost:8080/admin/events?users=\" + user.id +\"&states=PUBLISHED&categories=\" + category.id + \"&rangeStart=2022-01-06%2013%3A30%3A38&rangeEnd=2097-09-06%2013%3A30%3A38&from=0&size=1000\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " pm.collectionVariables.set('confirmedRequests', response.json()[0].confirmedRequests)\r", + " });\r", + " const requestToJoin = await api.publishParticipationRequest(event.id, user2.id);\r", + " const confirmedRequest = await api.acceptParticipationRequest(event.id, user.id, requestToJoin.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json()[0];\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, views, confirmedRequests, description, participantLimit, state, createdOn, publishedOn, location, requestModeration\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('annotation');\r", + "pm.expect(target).to.have.property('category');\r", + "pm.expect(target).to.have.property('paid');\r", + "pm.expect(target).to.have.property('eventDate');\r", + "pm.expect(target).to.have.property('initiator');\r", + "pm.expect(target).to.have.property('views');\r", + "pm.expect(target).to.have.property('confirmedRequests');\r", + "pm.expect(target).to.have.property('description');\r", + "pm.expect(target).to.have.property('participantLimit');\r", + "pm.expect(target).to.have.property('state');\r", + "pm.expect(target).to.have.property('createdOn');\r", + "pm.expect(target).to.have.property('publishedOn');\r", + "pm.expect(target).to.have.property('location');\r", + "pm.expect(target).to.have.property('requestModeration');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.annotation).equal(target.annotation, 'Аннотация события должна соответствовать искомому событию');\r", + " pm.expect(source.category.id).equal(target.category.id, 'Идентификатор категории должен соответствовать искомой категории');\r", + " pm.expect(source.paid.toString()).equal(target.paid.toString(), 'Стоимость посещения события должна соответствовать искомому событию');\r", + " pm.expect(source.eventDate).equal(target.eventDate, 'Дата проведения события должна соответствовать дате искомого события');\r", + " pm.expect(source.description).equal(target.description, 'Описание события должно соответствовать искомому событию');\r", + " pm.expect(source.title).equal(target.title, 'Название события должно соответствовать искомому событию');\r", + " pm.expect(source.participantLimit.toString()).equal(target.participantLimit.toString(), 'Число участников события должно соответствовать искомому событию');\r", + " pm.expect(pm.collectionVariables.get('confirmedRequests')).equal(0);\r", + " pm.expect(target.confirmedRequests).equal(1);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/events?users=0&states=PUBLISHED&categories=0&rangeStart=2022-01-06%2013%3A30%3A38&rangeEnd=2097-09-06%2013%3A30%3A38&from=0&size=1000", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events" + ], + "query": [ + { + "key": "users", + "value": "0", + "description": "список id пользователей, чьи события нужно найти" + }, + { + "key": "states", + "value": "PUBLISHED", + "description": "список состояний в которых находятся искомые события" + }, + { + "key": "categories", + "value": "0", + "description": "список id категорий в которых будет вестись поиск" + }, + { + "key": "rangeStart", + "value": "2022-01-06%2013%3A30%3A38", + "description": "дата и время не раньше которых должно произойти событие" + }, + { + "key": "rangeEnd", + "value": "2097-09-06%2013%3A30%3A38", + "description": "дата и время не позже которых должно произойти событие" + }, + { + "key": "from", + "value": "0", + "description": "количество событий, которые нужно пропустить для формирования текущего набора" + }, + { + "key": "size", + "value": "1000", + "description": "количество событий в наборе" + } + ] + }, + "description": "Эндпоинт возвращает полную информацию обо всех событиях подходящих под переданные условия" + }, + "response": [] + }, + { + "name": "Получение событий с возможностью фильтрации и проверкой на валидацию", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " \r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/events?text=0&categories=0&paid=true&rangeStart=2022-01-06%2013%3A30%3A38&rangeEnd=2007-09-06%2013%3A30%3A38&onlyAvailable=false&sort=EVENT_DATE&from=0&size=1000", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "events" + ], + "query": [ + { + "key": "text", + "value": "0", + "description": "текст для поиска в содержимом аннотации и подробном описании события" + }, + { + "key": "categories", + "value": "0", + "description": "список идентификаторов категорий в которых будет вестись поиск" + }, + { + "key": "paid", + "value": "true", + "description": "поиск только платных/бесплатных событий" + }, + { + "key": "rangeStart", + "value": "2022-01-06%2013%3A30%3A38", + "description": "дата и время не раньше которых должно произойти событие" + }, + { + "key": "rangeEnd", + "value": "2007-09-06%2013%3A30%3A38", + "description": "дата и время не позже которых должно произойти событие" + }, + { + "key": "onlyAvailable", + "value": "false", + "description": "только события у которых не исчерпан лимит запросов на участие" + }, + { + "key": "sort", + "value": "EVENT_DATE", + "description": "Вариант сортировки: по дате события или по количеству просмотров" + }, + { + "key": "from", + "value": "0", + "description": "количество событий, которые нужно пропустить для формирования текущего набора" + }, + { + "key": "size", + "value": "1000", + "description": "количество событий в наборе" + } + ] + }, + "description": "Обратите внимание: \n- это публичный эндпоинт, соответственно в выдаче должны быть только опубликованные события\n- текстовый поиск (по аннотации и подробному описанию) должен быть без учета регистра букв\n- если в запросе не указан диапазон дат [rangeStart-rangeEnd], то нужно выгружать события, которые произойдут позже текущей даты и времени\n- информация о каждом событии должна включать в себя количество просмотров и количество уже одобренных заявок на участие\n- информацию о том, что по этому эндпоинту был осуществлен и обработан запрос, нужно сохранить в сервисе статистики" + }, + "response": [] + }, + { + "name": "Проверка работы поля views", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " await api.findEvent(event.id)\r", + " await api.findEvent(event.id)\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, views, confirmedRequests, description, participantLimit, state, createdOn, publishedOn, location, requestModeration\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('annotation');\r", + "pm.expect(target).to.have.property('category');\r", + "pm.expect(target).to.have.property('paid');\r", + "pm.expect(target).to.have.property('eventDate');\r", + "pm.expect(target).to.have.property('initiator');\r", + "pm.expect(target).to.have.property('views');\r", + "pm.expect(target).to.have.property('confirmedRequests');\r", + "pm.expect(target).to.have.property('description');\r", + "pm.expect(target).to.have.property('participantLimit');\r", + "pm.expect(target).to.have.property('state');\r", + "pm.expect(target).to.have.property('createdOn');\r", + "pm.expect(target).to.have.property('publishedOn');\r", + "pm.expect(target).to.have.property('location');\r", + "pm.expect(target).to.have.property('requestModeration');\r", + "});\r", + "\r", + "pm.test(\"Значение поля views должно увеличится на 1 после выполнения GET запроса с уникального IP к событию\", function () {\r", + " pm.expect(target.views).equal(1);\r", + " \r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/events/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "events", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n- событие должно быть опубликовано\n- информация о событии должна включать в себя количество просмотров и количество подтвержденных запросов\n- информацию о том, что по этому эндпоинту был осуществлен и обработан запрос, нужно сохранить в сервисе статистики" + }, + "response": [] + }, + { + "name": "Добавление запроса на участие при participantLimit == 0", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " eventBody['participantLimit'] = 0\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " pm.request.removeQueryParams(['eventId']);\r", + " pm.request.addQueryParams([`eventId=` + event.id]);\r", + " pm.collectionVariables.set('uid', submittedUser.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201); \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "var query = {};\r", + "pm.request.url.query.all().forEach((param) => { query[param.key] = param.value});\r", + "\r", + "pm.test(\"Запрос на участие должен содержать поля: id, requester, event, status, created\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('requester');\r", + "pm.expect(target).to.have.property('event');\r", + "pm.expect(target).to.have.property('status');\r", + "pm.expect(target).to.have.property('created');\r", + "});\r", + "\r", + "pm.test(\"При создании у запроса на участие должен быть статус CONFIRMED\", function () {\r", + " pm.expect(target.status).equal(\"CONFIRMED\");\r", + "});\r", + "\r", + "pm.test(\"Id ивента в запросе и в ответе должны совпадать\", function () {\r", + " pm.expect(target.event.toString()).equal(query['eventId'].toString());\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/requests?eventId=0", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "requests" + ], + "query": [ + { + "key": "eventId", + "value": "0", + "description": "(Required) id события" + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- нельзя добавить повторный запрос\n- инициатор события не может добавить запрос на участие в своём событии\n- нельзя участвовать в неопубликованном событии\n- если у события достигнут лимит запросов на участие - необходимо вернуть ошибку\n- если для события отключена пре-модерация запросов на участие, то запрос должен автоматически перейти в состояние подтвержденного" + }, + "response": [] + }, + { + "name": "Изменение даты события на уже наступившую", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " eventDate : \"2020-10-11 23:10:05\"\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.have.status(400);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Изменение события добавленного текущим пользователем. Изменение даты не неподходящую", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.collectionVariables.set(\"response\", event);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " eventDate : \"2020-10-11 23:10:05\"\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.have.status(400);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Добавление события на неподходящую дату", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event.eventDate = \"2020-12-31 15:10:05\";\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.have.status(400);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + } + ] + }, + { + "name": "String length restrictions", + "item": [ + { + "name": "Добавление нового события с description.length < 20", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event.description = rnd.getWord(19);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление нового события с description.length < 20", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event.description = rnd.getWord(19);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление нового события с annotation.length < 20", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event.annotation = rnd.getWord(19);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление нового события с annotation.length > 2000", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event.annotation = rnd.getWord(2001);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление нового события с description.length == 20 && annotation.length == 20 && title.length == 3", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event.description = rnd.getWord(20);\r", + " event.annotation = rnd.getWord(20);\r", + " event.title = rnd.getWord(3);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление нового события с description.length == 7000 && annotation.length == 2000 && title.length == 120", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event.description = rnd.getWord(7000);\r", + " event.annotation = rnd.getWord(2000);\r", + " event.title = rnd.getWord(120);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление нового события с title.length < 3", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event.title = rnd.getWord(2);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление нового события с title.length > 120", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " event.title = rnd.getWord(121);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Изменение заголовка события с title.length < 3 (admin endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " let event2 = rnd.getEvent(category.id)\r", + " event2.title = rnd.getWord(2);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: event2,\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Изменение заголовка события с title.length > 120 (admin endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " let event2 = rnd.getEvent(category.id)\r", + " event2.title = rnd.getWord(121);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: event2,\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Изменение описания события с description.length < 20 (admin endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " let event2 = rnd.getEvent(category.id)\r", + " event2.description = rnd.getWord(19);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: event2,\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Изменение описания события с description.length > 7000 (admin endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " let event2 = rnd.getEvent(category.id)\r", + " event2.description = rnd.getWord(7001);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: event2,\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Изменение аннотации события с annotation.length < 20 (admin endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " let event2 = rnd.getEvent(category.id)\r", + " event2.annotation = rnd.getWord(19);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: event2,\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Изменение аннотации события с annotation.length > 2000 (admin endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " let event2 = rnd.getEvent(category.id)\r", + " event2.annotation = rnd.getWord(2001);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: event2,\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Изменение события с description.length == 20 && annotation.length == 20 && title.length == 3 (admin endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " let event2 = rnd.getEvent(category.id)\r", + " event2.annotation = rnd.getWord(20);\r", + " event2.description = rnd.getWord(20);\r", + " event2.title = rnd.getWord(3);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: event2,\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Изменение события с description.length == 7000 && annotation.length == 2000 && title.length == 120 (admin endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " let event2 = rnd.getEvent(category.id)\r", + " event2.annotation = rnd.getWord(2000);\r", + " event2.description = rnd.getWord(7000);\r", + " event2.title = rnd.getWord(120);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: event2,\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Изменение заголовка события с title.length < 3 (user endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " title: rnd.getWord(2)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Изменение заголовка события с title.length > 120 (user endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " title: rnd.getWord(121)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Изменение описания события с description.length < 20 (user endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " description: rnd.getWord(19)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Изменение описания события с description.length > 7000 (user endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " description: rnd.getWord(7001)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Изменение аннотации события с annotation.length < 20 (user endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " annotation: rnd.getWord(19)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Изменение аннотации события с annotation.length > 2000 (user endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " annotation: rnd.getWord(2001)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Изменение события с description.length == 20 && annotation.length == 20 && title.length == 3 (user endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " description: rnd.getWord(20),\r", + " annotation: rnd.getWord(20),\r", + " title: rnd.getWord(3)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Изменение события с description.length == 7000 && annotation.length == 2000 && title.length == 120 (user endpoint)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " description: rnd.getWord(7000),\r", + " annotation: rnd.getWord(2000),\r", + " title: rnd.getWord(120)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + } + ] + }, + { + "name": "Default values check", + "item": [ + { + "name": "Добавление нового события без paid, participantLimit, requestModeration", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let usersArr = Array.from({length: 10}, () => rnd.getUser());\r", + " let categoriesArr = Array.from({length: 10}, () => rnd.getCategory());\r", + " let usersResponseArr = [], categoriesResponseArr = [], eventArr, eventResponseArr = [];\r", + " try {\r", + " for (const u of usersArr){\r", + " usersResponseArr.push(await api.addUser(u));\r", + " }\r", + " for (const c of categoriesArr){\r", + " categoriesResponseArr.push(await api.addCategory(c));\r", + " }\r", + " eventArr = Array.from(categoriesResponseArr, (x) => rnd.getEvent(x.id));\r", + " for (let i = 0; i < 10; i++){\r", + " delete eventArr[i].requestModeration;\r", + " delete eventArr[i].paid;\r", + " delete eventArr[i].participantLimit;\r", + " }\r", + " for (let i = 0; i < 10; i++){\r", + " eventResponseArr.push(await api.addEvent(usersResponseArr[i].id, eventArr[i]));\r", + " }\r", + " pm.collectionVariables.set('responseArr', eventResponseArr)\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201); \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('responseArr');\r", + "\r", + "\r", + "pm.test(\"У каждого созданного события paid должно принять значение по умолчанию(false)\", function () {\r", + " source.forEach(function(x){pm.expect(x.paid).to.be.equal(false)});\r", + "});\r", + "\r", + "pm.test(\"У каждого созданного события participantLimit должен принять значение по умолчанию(0)\", function () {\r", + " source.forEach(function(x){pm.expect(x.participantLimit).to.be.equal(0)});\r", + "});\r", + "\r", + "pm.test(\"У каждого созданного события requestModeration должно принять значение по умолчанию(true)\", function () {\r", + " source.forEach(function(x){pm.expect(x.requestModeration).to.be.equal(true)});\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Проверка на значения по-умолчанию from и size(event)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user, category, eventArr;\r", + " try {\r", + " user = await api.addUser(rnd.getUser());\r", + " category = await api.addCategory(rnd.getCategory());\r", + " eventArr = Array.from({length:11}, () => rnd.getEvent(category.id));\r", + " for (let i = 0; i < 11; i++){\r", + " await api.addEvent(user.id, eventArr[i]);\r", + " }\r", + " await pm.sendRequest({\r", + " url : \"http://localhost:8080/admin/events?from=0\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {pm.collectionVariables.set('source', response.json())});\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "const source = pm.collectionVariables.get('source');\r", + "\r", + "pm.test(\"Значение from по-умолчанию должно быть равным 0\", function () {\r", + " pm.expect(target[0].id).to.be.equal(source[0].id, 'Запросы с from=0 и без него должны начинаться с одного и того же события');\r", + "});\r", + "\r", + "pm.test(\"Значение size по-умолчанию должно быть равным 10\", function () {\r", + " pm.expect(target.length).to.be.equal(10);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events" + ], + "query": [ + { + "key": "users", + "value": "0", + "description": "список id пользователей, чьи события нужно найти", + "disabled": true + }, + { + "key": "states", + "value": "PUBLISHED", + "description": "список состояний в которых находятся искомые события", + "disabled": true + }, + { + "key": "categories", + "value": "0", + "description": "список id категорий в которых будет вестись поиск", + "disabled": true + }, + { + "key": "rangeStart", + "value": "2022-01-06%2013%3A30%3A38", + "description": "дата и время не раньше которых должно произойти событие", + "disabled": true + }, + { + "key": "rangeEnd", + "value": "2097-09-06%2013%3A30%3A38", + "description": "дата и время не позже которых должно произойти событие", + "disabled": true + }, + { + "key": "from", + "value": "0", + "description": "количество событий, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "1000", + "description": "количество событий в наборе", + "disabled": true + } + ] + }, + "description": "Эндпоинт возвращает полную информацию обо всех событиях подходящих под переданные условия" + }, + "response": [] + }, + { + "name": "Получение событий, добавленных текущим пользователем. Проверка на значения по-умолчанию size и from", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let eventArr, user, category, eventResponseArr = [];\r", + " try {\r", + " user = await api.addUser(rnd.getUser());\r", + " category = await api.addCategory(rnd.getCategory());\r", + " eventArr = Array.from({length:11}, () => rnd.getEvent(category.id));\r", + " for (let i = 0; i < 11; i++){\r", + " eventResponseArr.push(await api.addEvent(user.id, eventArr[i]));\r", + " }\r", + " pm.collectionVariables.set('responseArr', eventResponseArr)\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " await pm.sendRequest({\r", + " url : \"http://localhost:8080/users/\" + user.id + \"/events?from=0\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {pm.collectionVariables.set('source', response.json())});\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "const source = pm.collectionVariables.get('responseArr');\r", + "newSourceArr = Array.from(source, (x) => x.id);\r", + "const responseWithFrom = pm.collectionVariables.get('source');\r", + "\r", + "pm.test(\"Значение size по-умолчанию должно быть равным 10\", function () {\r", + " pm.expect(target.length).to.equal(10);\r", + "});\r", + "\r", + "pm.test(\"Значение from по-умолчанию должно быть равным 0\", function () {\r", + " pm.expect(target[0].id).to.be.equal(responseWithFrom[0].id, 'Запросы с from=0 и без него должны начинаться с одного и того же события');\r", + "});\r", + "\r", + "pm.test(\"Все найденные события должны быть в списке добавленных\", function () {\r", + " source.forEach(function(x){pm.expect(newSourceArr).to.include(x.id)});\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "query": [ + { + "key": "from", + "value": "0", + "description": "количество элементов, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "1000", + "description": "количество элементов в наборе", + "disabled": true + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + } + }, + "response": [] + }, + { + "name": "Получение событий с возможностью фильтрации. Проверка на значение по-умолчанию size", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user, category, eventArr, eventResponseArr = [], publishEventResponseArr = [];\r", + " try {\r", + " user = await api.addUser(rnd.getUser());\r", + " category = await api.addCategory(rnd.getCategory());\r", + " eventArr = Array.from({length:11}, () => rnd.getEvent(category.id));\r", + " for (let i = 0; i < 11; i++){\r", + " eventResponseArr.push(await api.addEvent(user.id, eventArr[i]));\r", + " }\r", + " for (let i = 0; i < 11; i++){\r", + " publishEventResponseArr.push(await api.publishEvent(eventResponseArr[i].id));\r", + " }\r", + " pm.collectionVariables.set('responseArr', eventResponseArr);\r", + " pm.collectionVariables.set('catid', category.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "const source = pm.collectionVariables.get('responseArr');\r", + "newSourceArr = Array.from(source, (x) => x.id);\r", + "\r", + "pm.test(\"Значение size по-умолчанию должно быть равным 10\", function () {\r", + " pm.expect(target.length).to.equal(10);\r", + "});\r", + "\r", + "pm.test(\"Все найденные события должны быть в списке добавленных\", function () {\r", + " source.forEach(function(x){pm.expect(newSourceArr).to.include(x.id)});\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/events?categories={{catid}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "events" + ], + "query": [ + { + "key": "text", + "value": "0", + "description": "текст для поиска в содержимом аннотации и подробном описании события", + "disabled": true + }, + { + "key": "categories", + "value": "{{catid}}", + "description": "список идентификаторов категорий в которых будет вестись поиск" + }, + { + "key": "paid", + "value": "true", + "description": "поиск только платных/бесплатных событий", + "disabled": true + }, + { + "key": "rangeStart", + "value": "2022-01-06%2013%3A30%3A38", + "description": "дата и время не раньше которых должно произойти событие", + "disabled": true + }, + { + "key": "rangeEnd", + "value": "2097-09-06%2013%3A30%3A38", + "description": "дата и время не позже которых должно произойти событие", + "disabled": true + }, + { + "key": "onlyAvailable", + "value": "false", + "description": "только события у которых не исчерпан лимит запросов на участие", + "disabled": true + }, + { + "key": "sort", + "value": "EVENT_DATE", + "description": "Вариант сортировки: по дате события или по количеству просмотров", + "disabled": true + }, + { + "key": "from", + "value": "0", + "description": "количество событий, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "1000", + "description": "количество событий в наборе", + "disabled": true + } + ] + }, + "description": "Обратите внимание: \n- это публичный эндпоинт, соответственно в выдаче должны быть только опубликованные события\n- текстовый поиск (по аннотации и подробному описанию) должен быть без учета регистра букв\n- если в запросе не указан диапазон дат [rangeStart-rangeEnd], то нужно выгружать события, которые произойдут позже текущей даты и времени\n- информация о каждом событии должна включать в себя количество просмотров и количество уже одобренных заявок на участие\n- информацию о том, что по этому эндпоинту был осуществлен и обработан запрос, нужно сохранить в сервисе статистики" + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Users", + "item": [ + { + "name": "Required query params", + "item": [] + }, + { + "name": "Unrequired query params", + "item": [ + { + "name": "Поиск пользователей без нескольких Query params", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/users?ids={{uid}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ], + "query": [ + { + "key": "ids", + "value": "{{uid}}", + "description": "id пользователей" + }, + { + "key": "from", + "value": "0", + "description": "количество элементов, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "10", + "description": "количество элементов в наборе", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Поиск пользователей без параметра ids", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + " let user1 = rnd.getUser();\r", + " user1 = await api.addUser(user1);\r", + "\r", + " let user2 = rnd.getUser()\r", + " user2 = await api.addUser(user2);\r", + " //pm.collectionVariables.set('fromId', user1.id);\r", + " pm.collectionVariables.set('source1', user1);\r", + " pm.collectionVariables.set('source2', user2);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "const source1 = pm.collectionVariables.get('source1');\r", + "const source2 = pm.collectionVariables.get('source2');\r", + "\r", + "pm.test(\"Пользователи должны содержать поля: id, name, email\", function () {\r", + " pm.expect(target[target.length-2]).to.have.property('id');\r", + " pm.expect(target[target.length-2]).to.have.property('name');\r", + " pm.expect(target[target.length-2]).to.have.property('email');\r", + " pm.expect(target[target.length-1]).to.have.property('id');\r", + " pm.expect(target[target.length-1]).to.have.property('name');\r", + " pm.expect(target[target.length-1]).to.have.property('email');\r", + "});\r", + "\r", + "pm.test(\"Данные последних двух пользователей должны совпадать с данными добавленных пользователей\", function () {\r", + " pm.expect(target[target.length-2].id).to.equal(source1.id);\r", + " pm.expect(target[target.length-2].name).to.equal(source1.name);\r", + " pm.expect(target[target.length-2].email).to.equal(source1.email);\r", + " pm.expect(target[target.length-1].id).to.equal(source2.id);\r", + " pm.expect(target[target.length-1].name).to.equal(source2.name);\r", + " pm.expect(target[target.length-1].email).to.equal(source2.email);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/users?from={{fromId}}&size=100000", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ], + "query": [ + { + "key": "ids", + "value": "{{uid}}", + "description": "id пользователей", + "disabled": true + }, + { + "key": "from", + "value": "{{fromId}}", + "description": "количество элементов, которые нужно пропустить для формирования текущего набора" + }, + { + "key": "size", + "value": "100000", + "description": "количество элементов в наборе" + } + ] + } + }, + "response": [] + }, + { + "name": "Поиск событий без нескольких Query params", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " pm.request.removeQueryParams(['users', 'categories']);\r", + " pm.request.addQueryParams([`users=` + user.id, 'categories=' + category.id]);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events" + ], + "query": [ + { + "key": "users", + "value": "0", + "description": "список id пользователей, чьи события нужно найти", + "disabled": true + }, + { + "key": "states", + "value": "PUBLISHED", + "description": "список состояний в которых находятся искомые события", + "disabled": true + }, + { + "key": "categories", + "value": "0", + "description": "список id категорий в которых будет вестись поиск", + "disabled": true + }, + { + "key": "rangeStart", + "value": "2022-01-06%2013%3A30%3A38", + "description": "дата и время не раньше которых должно произойти событие", + "disabled": true + }, + { + "key": "rangeEnd", + "value": "2097-09-06%2013%3A30%3A38", + "description": "дата и время не позже которых должно произойти событие", + "disabled": true + }, + { + "key": "from", + "value": "0", + "description": "количество событий, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "1000", + "description": "количество событий в наборе", + "disabled": true + } + ] + }, + "description": "Эндпоинт возвращает полную информацию обо всех событиях подходящих под переданные условия" + }, + "response": [] + } + ] + }, + { + "name": "Required params in body", + "item": [ + { + "name": "Добавление пользователя с электронной почтой, состоящей только из пробелов", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.email = \" \";\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400\", function () {\r", + " pm.response.to.have.status(400);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с пустой электронной почтой", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.email = \"\";\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400\", function () {\r", + " pm.response.to.have.status(400);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя без поля email", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " delete user.email;\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400\", function () {\r", + " pm.response.to.have.status(400);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с пустым именем", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.name = \"\";\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400\", function () {\r", + " pm.response.to.have.status(400);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с именем, состоящим только из пробелов", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.name = \" \";\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400\", function () {\r", + " pm.response.to.have.status(400);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя без поля name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " delete user.name;\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400\", function () {\r", + " pm.response.to.have.status(400);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Misc tests", + "item": [] + }, + { + "name": "String length restrictions", + "item": [ + { + "name": "Добавление пользователя с name.length < 2", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.name = rnd.getWord(1);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с name.length == 2", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.name = rnd.getWord(2);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с name.length > 250", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.name = rnd.getWord(251);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с name.length == 250", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.name = rnd.getWord(250);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с email.length < 6", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.email = rnd.getWord(1) + '@a.r';\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с email.length == 6", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.email = rnd.getWord(1) + '@a.ru';\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с email.localpart.length > 64", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.email = rnd.getWord(65) + '@a.ru';\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с email.localpart.length == 64", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.email = rnd.getWord(59) + '@a.ru';\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с domain.part.length > 63", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.email = rnd.getWord(1) + '@' + rnd.getWord(64) + '.ru';\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с domain.part.length == 63", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.email = rnd.getWord(1) + '@' + rnd.getWord(60) + '.ru';\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с email.length > 254", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.email = rnd.getWord(1) + '@' + rnd.getWord(63) + '.' + rnd.getWord(63) + '.' + rnd.getWord(63) + '.' + rnd.getWord(61);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление пользователя с email.length == 254", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.email = rnd.getWord(1) + '@' + rnd.getWord(63) + '.' + rnd.getWord(63) + '.' + rnd.getWord(63) + '.' + rnd.getWord(60);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Default values check", + "item": [ + { + "name": "Проверка на значения по-умолчанию from и size(user)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " for (let i = 0; i < 11; i++){\r", + " await api.addUser(rnd.getUser());\r", + " }\r", + " await pm.sendRequest({\r", + " url : \"http://localhost:8080/admin/users?from=0\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {pm.collectionVariables.set('source', response.json())});\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "const source = pm.collectionVariables.get('source');\r", + "\r", + "pm.test(\"Значение from по-умолчанию должно быть равным 0\", function () {\r", + " pm.expect(target[0].id).to.be.equal(source[0].id, 'Запросы с from=0 и без него должны начинаться с одного и того же события');\r", + "});\r", + "\r", + "pm.test(\"Значение size по-умолчанию должно быть равным 10\", function () {\r", + " pm.expect(target.length).to.be.equal(10);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ], + "query": [ + { + "key": "ids", + "value": "{{uid}}", + "description": "id пользователей", + "disabled": true + }, + { + "key": "ids", + "value": "-10833646", + "description": "id пользователей", + "disabled": true + }, + { + "key": "from", + "value": "0", + "description": "количество элементов, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "10", + "description": "количество элементов в наборе", + "disabled": true + } + ] + } + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Category", + "item": [ + { + "name": "Required query params", + "item": [] + }, + { + "name": "Unrequired params in body", + "item": [ + { + "name": "Получение категорий без нескольких Query params", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/categories", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "categories" + ], + "query": [ + { + "key": "from", + "value": "0", + "description": "количество категорий, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "10000", + "description": "количество категорий в наборе", + "disabled": true + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Required params in body", + "item": [ + { + "name": "Добавление категории с именем, состоящим из пробелов", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let category;\r", + " try {\r", + " category = {name: ' '};\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(category),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{baseUrl}}/admin/categories", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories" + ] + } + }, + "response": [] + }, + { + "name": "Добавление категории с пустым полем name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let category;\r", + " try {\r", + " category = {name: ''};\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(category),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{baseUrl}}/admin/categories", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories" + ] + } + }, + "response": [] + }, + { + "name": "Добавление категории без поля name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let category;\r", + " try {\r", + " category = {};\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(category),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{baseUrl}}/admin/categories", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Misc tests", + "item": [ + { + "name": "Изменение категории с неизменными данными", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + " let category, categoryObj\r", + " try {\r", + " category = rnd.getCategory();\r", + " categoryObj = await api.addCategory(category);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.collectionVariables.set(\"catid\", Number(categoryObj.id))\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(category),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Категория должна содержать поля: id, name\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('name');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + " pm.expect(source.name).equal(target.name, 'Название категории должно совпадать с отправленным');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/categories/:catId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories", + ":catId" + ], + "variable": [ + { + "key": "catId", + "value": "{{catid}}" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "String length restrictions", + "item": [ + { + "name": "Добавление новой категории с name.length > 50", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let category;\r", + " try {\r", + " category = {'name': rnd.getWord(51)};\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(category),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/categories", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories" + ] + }, + "description": "Обратите внимание: имя категории должно быть уникальным" + }, + "response": [] + }, + { + "name": "Добавление новой категории с name.length == 50", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let category;\r", + " try {\r", + " category = {'name': rnd.getWord(50)};\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(category),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/categories", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories" + ] + }, + "description": "Обратите внимание: имя категории должно быть уникальным" + }, + "response": [] + }, + { + "name": "Изменение имени категории с name.length > 50", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + " let category\r", + " try {\r", + " category = await api.addCategory(rnd.getCategory());\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.collectionVariables.set(\"catid\", Number(category.id))\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " name : rnd.getWord(51)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/categories/:catId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories", + ":catId" + ], + "variable": [ + { + "key": "catId", + "value": "{{catid}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Изменение имени категории с name.length == 50", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + " let category\r", + " try {\r", + " category = await api.addCategory(rnd.getCategory());\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.collectionVariables.set(\"catid\", Number(category.id))\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " name : rnd.getWord(50)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/categories/:catId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories", + ":catId" + ], + "variable": [ + { + "key": "catId", + "value": "{{catid}}" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Default values check", + "item": [ + { + "name": "Проверка на значения по-умолчанию from и size(category)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " for (let i = 0; i < 11; i++){\r", + " await api.addCategory(rnd.getCategory());\r", + " }\r", + " await pm.sendRequest({\r", + " url : \"http://localhost:8080/categories?from=0\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {pm.collectionVariables.set('source', response.json())});\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "const source = pm.collectionVariables.get('source');\r", + "\r", + "pm.test(\"Значение from по-умолчанию должно быть равным 0\", function () {\r", + " pm.expect(target[0].id).to.be.equal(source[0].id, 'Запросы с from=0 и без него должны начинаться с одного и того же события');\r", + "});\r", + "\r", + "pm.test(\"Значение size по-умолчанию должно быть равным 10\", function () {\r", + " pm.expect(target.length).to.be.equal(10);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/categories", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "categories" + ], + "query": [ + { + "key": "from", + "value": "0", + "description": "количество категорий, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "1000", + "description": "количество категорий в наборе", + "disabled": true + } + ] + } + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Compilation", + "item": [ + { + "name": "Required query params", + "item": [] + }, + { + "name": "Unreqired params in body", + "item": [ + { + "name": "Получение подборок событий без нескольких Query params", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const compilation = await api.addCompilation(rnd.getCompilation());\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "compilations" + ], + "query": [ + { + "key": "pinned", + "value": "true", + "description": "искать только закрепленные/не закрепленные подборки", + "disabled": true + }, + { + "key": "from", + "value": "0", + "description": "количество элементов, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "1000", + "description": "количество элементов в наборе", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Добавление новой подборки без параметра pinned", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " compilation = rnd.getCompilation(event.id);\r", + " delete compilation['pinned'];\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(compilation),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Подборка должны содержать поля: id, title, pinned, events\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('pinned');\r", + "pm.expect(target).to.have.property('events');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + " pm.expect(target.title).to.be.a(\"string\");\r", + " pm.expect(target.events).to.be.an(\"array\");\r", + " pm.expect(target.pinned).equal(false);\r", + "\r", + " pm.expect(source.events[0]).equal(target.events[0].id, 'Идентификаторы событий в подборке должен быть идентичен идентификаторам, указанным при создании подборки ');\r", + " pm.expect(source.title).equal(target.title, 'Название подборки должно соответствовать указанному при создании');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Required params in body", + "item": [ + { + "name": "Добавление подборки без поля title", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " compilation = {\r", + " \"pinned\":\"true\"};\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(compilation),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations" + ] + } + }, + "response": [] + }, + { + "name": "Добавление подборки с пустым полем title", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " compilation = {\r", + " \"pinned\":\"true\",\r", + " \"title\": \"\"};\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(compilation),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations" + ] + } + }, + "response": [] + }, + { + "name": "Добавление подборки с пустой строкой в качестве названия", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " compilation = {\r", + " \"pinned\":\"true\",\r", + " \"title\": \" \"};\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(compilation),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Misc tests", + "item": [ + { + "name": "Добавление подборки с проверкой связей многие-ко-многим", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " let compilation2;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " const event2 = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " compilation = rnd.getCompilation(event.id, event2.id);\r", + " compilation2 = rnd.getCompilation(event.id, event2.id);\r", + " await pm.sendRequest({\r", + " url : \"http://localhost:8080/admin/compilations/\",\r", + " method : \"POST\",\r", + " header: { \"Content-Type\": \"application/json\" },\r", + " body: JSON.stringify({\r", + " events: compilation2.events,\r", + " title: compilation2.title\r", + " })\r", + " }, (error, response) => {\r", + "\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(compilation),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Подборка должны содержать поля: id, title, pinned, events\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('pinned');\r", + "pm.expect(target).to.have.property('events');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + " pm.expect(target.title).to.be.a(\"string\");\r", + " pm.expect(target.events).to.be.an(\"array\");\r", + " if (target.events[0].id < target.events[1].id){\r", + " pm.expect(source.events[0]).equal(target.events[0].id, 'Идентификаторы событий в подборке должен быть идентичен идентификаторам, указанным при создании подборки ');\r", + " } else {\r", + " pm.expect(source.events[0]).equal(target.events[1].id, 'Идентификаторы событий в подборке должен быть идентичен идентификаторам, указанным при создании подборки ');\r", + " }\r", + " pm.expect(source.title).equal(target.title, 'Название подборки должно соответствовать указанному при создании');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations" + ] + } + }, + "response": [] + }, + { + "name": "Добавление подборки без событий", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " compilation = rnd.getCompilation();\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(compilation),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Подборка должны содержать поля: id, title, pinned, events\", function () {\r", + " pm.expect(target).to.have.property('id');\r", + " pm.expect(target).to.have.property('title');\r", + " pm.expect(target).to.have.property('pinned');\r", + " pm.expect(target).to.have.property('events');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + " pm.expect(target.title).to.be.a(\"string\");\r", + " pm.expect(target.events).to.be.an(\"array\");\r", + "\r", + " pm.expect(source.title).equal(target.title, 'Название подборки должно соответствовать указанному при создании');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "String length restrictions", + "item": [ + { + "name": "Добавление подборки с title.length > 50", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({title: rnd.getWord(51)}),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations" + ] + } + }, + "response": [] + }, + { + "name": "Добавление подборки с title.length == 50", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({title: rnd.getWord(50)}),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations" + ] + } + }, + "response": [] + }, + { + "name": "Обновить названия подборки с title.length > 50", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const compilation = await api.addCompilation(rnd.getCompilation());\r", + " pm.collectionVariables.set('compid', compilation.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " title: rnd.getWord(51)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations/:compId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations", + ":compId" + ], + "variable": [ + { + "key": "compId", + "value": "{{compid}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Обновить названия подборки с title.length == 50", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const compilation = await api.addCompilation(rnd.getCompilation());\r", + " pm.collectionVariables.set('compid', compilation.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " title: rnd.getWord(50)\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.have.status(200);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations/:compId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations", + ":compId" + ], + "variable": [ + { + "key": "compId", + "value": "{{compid}}" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Default values check", + "item": [ + { + "name": "Добавление подборки без pinned", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let cArr = Array.from({length: 10}, () => rnd.getCompilation());\r", + " let responseArr = [];\r", + " try {\r", + " cArr.forEach(function(x){ delete x.pinned });\r", + " for (const c of cArr){\r", + " responseArr.push(await api.addCompilation(c));\r", + " }\r", + " pm.collectionVariables.set('responseArr', responseArr);\r", + " compilation = rnd.getCompilation();\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(compilation),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('responseArr');\r", + "\r", + "\r", + "pm.test(\"У каждой из созданных подборок pinned должно принять значение по умолчанию(false)\", function () {\r", + " source.forEach(function(x){pm.expect(x.pinned).to.be.equal(false)});\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations" + ] + } + }, + "response": [] + }, + { + "name": "Проверка на значения по-умолчанию from и size(compilation)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " for (let i = 0; i < 11; i++){\r", + " await api.addCompilation(rnd.getCompilation());\r", + " }\r", + " await pm.sendRequest({\r", + " url : \"http://localhost:8080/compilations?from=0\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {pm.collectionVariables.set('source', response.json())});\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "const source = pm.collectionVariables.get('source');\r", + "\r", + "pm.test(\"Значение from по-умолчанию должно быть равным 0\", function () {\r", + " pm.expect(target[0].id).to.be.equal(source[0].id, 'Запросы с from=0 и без него должны начинаться с одного и того же события');\r", + "});\r", + "\r", + "pm.test(\"Значение size по-умолчанию должно быть равным 10\", function () {\r", + " pm.expect(target.length).to.be.equal(10);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "compilations" + ], + "query": [ + { + "key": "pinned", + "value": "true", + "description": "искать только закрепленные/не закрепленные подборки", + "disabled": true + }, + { + "key": "from", + "value": "0", + "description": "количество элементов, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "1000", + "description": "количество элементов в наборе", + "disabled": true + } + ] + } + }, + "response": [] + } + ] + } + ] + } + ] + }, + { + "name": "409 Conflict", + "item": [ + { + "name": "Попытка изменения имени категории на уже существующее", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + " let category1, category2\r", + " try {\r", + " category1 = await api.addCategory(rnd.getCategory());\r", + " category2 = await api.addCategory(rnd.getCategory());\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.collectionVariables.set(\"catid\", category2.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " name : category1.name\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/categories/:catId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories", + ":catId" + ], + "variable": [ + { + "key": "catId", + "value": "{{catid}}" + } + ] + }, + "description": "Обратите внимание: имя категории должно быть уникальным" + }, + "response": [] + }, + { + "name": "Добавление новой категории с занятым именем", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let category;\r", + " try {\r", + " category = rnd.getCategory();\r", + " await api.addCategory(category);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(category),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/categories", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories" + ] + }, + "description": "Обратите внимание: имя категории должно быть уникальным" + }, + "response": [] + }, + { + "name": "Добавление пользователя с занятым именем почты", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " user.name = rnd.getWord(10);\r", + " await api.addUser(user);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Удаление категории с привязанными событиями", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const user = await api.addUser(rnd.getUser());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set('catid', category.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/categories/:catId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories", + ":catId" + ], + "variable": [ + { + "key": "catId", + "value": "{{catid}}" + } + ] + }, + "description": "Обратите внимание: с категорий не должно быть связано ни одного события." + }, + "response": [] + }, + { + "name": "Изменение имени категории на уже занятое", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + " let category1, category2\r", + " try {\r", + " category1 = await api.addCategory(rnd.getCategory());\r", + " category2 = await api.addCategory(rnd.getCategory());\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.collectionVariables.set(\"catid\", Number(category1.id))\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " name : category2.name\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/categories/:catId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories", + ":catId" + ], + "variable": [ + { + "key": "catId", + "value": "{{catid}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Публикация уже опубликованного события", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " stateAction : \"PUBLISH_EVENT\"\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Публикация отмененного события", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.rejectEvent(event.id);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " stateAction : \"PUBLISH_EVENT\"\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Отмена опубликованного события", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " stateAction : \"REJECT_EVENT\"\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Добавление повторного запроса от пользователя на участие в событии", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " await api.publishParticipationRequest(event.id, submittedUser.id);\r", + " pm.request.removeQueryParams(['eventId']);\r", + " pm.request.addQueryParams([`eventId=` + event.id]);\r", + " pm.collectionVariables.set('uid', submittedUser.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/requests?eventId=0", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "requests" + ], + "query": [ + { + "key": "eventId", + "value": "0", + "description": "(Required) id события" + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- нельзя добавить повторный запрос\n- инициатор события не может добавить запрос на участие в своём событии\n- нельзя участвовать в неопубликованном событии\n- если у события достигнут лимит запросов на участие - необходимо вернуть ошибку\n- если для события отключена пре-модерация запросов на участие, то запрос должен автоматически перейти в состояние подтвержденного" + }, + "response": [] + }, + { + "name": "Добавление запроса от инициатора события на участие в нём", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " pm.request.removeQueryParams(['eventId']);\r", + " pm.request.addQueryParams([`eventId=` + event.id]);\r", + " pm.collectionVariables.set('uid', user.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/requests?eventId=0", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "requests" + ], + "query": [ + { + "key": "eventId", + "value": "0", + "description": "(Required) id события" + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- нельзя добавить повторный запрос\n- инициатор события не может добавить запрос на участие в своём событии\n- нельзя участвовать в неопубликованном событии\n- если у события достигнут лимит запросов на участие - необходимо вернуть ошибку\n- если для события отключена пре-модерация запросов на участие, то запрос должен автоматически перейти в состояние подтвержденного" + }, + "response": [] + }, + { + "name": "Добавление запроса на участие в неопубликованном событии", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " pm.request.removeQueryParams(['eventId']);\r", + " pm.request.addQueryParams([`eventId=` + event.id]);\r", + " pm.collectionVariables.set('uid', submittedUser.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/requests?eventId=0", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "requests" + ], + "query": [ + { + "key": "eventId", + "value": "0", + "description": "(Required) id события" + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- нельзя добавить повторный запрос\n- инициатор события не может добавить запрос на участие в своём событии\n- нельзя участвовать в неопубликованном событии\n- если у события достигнут лимит запросов на участие - необходимо вернуть ошибку\n- если для события отключена пре-модерация запросов на участие, то запрос должен автоматически перейти в состояние подтвержденного" + }, + "response": [] + }, + { + "name": "Добавление запроса на участие в событии, у которого заполнен лимит участников", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody.participantLimit = 1;\r", + " eventBody.requestModeration = false;\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser1 = await api.addUser(rnd.getUser());\r", + " const submittedUser2 = await api.addUser(rnd.getUser());\r", + " await api.publishParticipationRequest(event.id, submittedUser1.id);\r", + " pm.request.removeQueryParams(['eventId']);\r", + " pm.request.addQueryParams([`eventId=` + event.id]);\r", + " pm.collectionVariables.set('uid', submittedUser2.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/requests?eventId=0", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "requests" + ], + "query": [ + { + "key": "eventId", + "value": "0", + "description": "(Required) id события" + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- нельзя добавить повторный запрос\n- инициатор события не может добавить запрос на участие в своём событии\n- нельзя участвовать в неопубликованном событии\n- если у события достигнут лимит запросов на участие - необходимо вернуть ошибку\n- если для события отключена пре-модерация запросов на участие, то запрос должен автоматически перейти в состояние подтвержденного" + }, + "response": [] + }, + { + "name": "Изменение опубликованного события от имени пользователя", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.collectionVariables.set(\"response\", event);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " eventDate : \"2124-10-11 23:10:05\"\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Попытка принять заявку на участие в событии, когда лимит уже достигнут", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true;\r", + " eventBody['participantLimit'] = 1;\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser1 = await api.addUser(rnd.getUser());\r", + " const submittedUser2 = await api.addUser(rnd.getUser());\r", + " const requestToParticipate = await api.publishParticipationRequest(event.id, submittedUser1.id);\r", + " const requestToParticipate2 = await api.publishParticipationRequest(event.id, submittedUser2.id);\r", + " await api.acceptParticipationRequest(event.id, user.id, requestToParticipate.id);\r", + " pm.collectionVariables.set('uid', user.id);\r", + " pm.collectionVariables.set('eid', event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({requestIds: [requestToParticipate2.id],\r", + " status:\"CONFIRMED\"}),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/requests", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "requests" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- если для события лимит заявок равен 0 или отключена пре-модерация заявок, то подтверждение заявок не требуется\n- нельзя подтвердить заявку, если уже достигнут лимит по заявкам на данное событие\n- статус можно изменить только у заявок, находящихся в состоянии ожидания\n- если при подтверждении данной заявки, лимит заявок для события исчерпан, то все неподтверждённые заявки необходимо отклонить" + }, + "response": [] + }, + { + "name": "Попытка отменить уже принятую заявку на участие в событии", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true;\r", + " eventBody['participantLimit'] = 1;\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " const requestToParticipate = await api.publishParticipationRequest(event.id, submittedUser.id);\r", + " await api.acceptParticipationRequest(event.id, user.id, requestToParticipate.id);\r", + " pm.collectionVariables.set('uid', user.id);\r", + " pm.collectionVariables.set('eid', event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({requestIds: [requestToParticipate.id],\r", + " status:\"REJECTED\"}),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 409 и данные в формате json\", function () {\r", + " pm.response.to.have.status(409);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/requests", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "requests" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- если для события лимит заявок равен 0 или отключена пре-модерация заявок, то подтверждение заявок не требуется\n- нельзя подтвердить заявку, если уже достигнут лимит по заявкам на данное событие\n- статус можно изменить только у заявок, находящихся в состоянии ожидания\n- если при подтверждении данной заявки, лимит заявок для события исчерпан, то все неподтверждённые заявки необходимо отклонить" + }, + "response": [] + } + ] + }, + { + "name": "Category", + "item": [ + { + "name": "Добавление новой категории", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let category;\r", + " try {\r", + " category = rnd.getCategory();\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(category),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Категория должна содержать поля: id, name\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('name');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + " pm.expect(source.name).equal(target.name, 'Название категории должно совпадать с отправленным');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/categories", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories" + ] + }, + "description": "Обратите внимание: имя категории должно быть уникальным" + }, + "response": [] + }, + { + "name": "Получение категорий", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " pm.collectionVariables.set(\"response\", category)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json();\r", + "let founded;\r", + "target.forEach(function(element){if (element.id == source.id) founded = element});\r", + "\r", + "pm.test(\"Категория должна содержать поля: id, name\", function () {\r", + "pm.expect(target[0]).to.have.property('id');\r", + "pm.expect(target[0]).to.have.property('name');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.id).equal(founded.id, 'Идентификатор категории должен соответствовать идентификатору категории добавленной ранее');\r", + " pm.expect(source.name).equal(founded.name, 'Название категории должно соответствовать названию категории добавленной ранее');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/categories?from=0&size=1000", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "categories" + ], + "query": [ + { + "key": "from", + "value": "0", + "description": "количество категорий, которые нужно пропустить для формирования текущего набора" + }, + { + "key": "size", + "value": "1000", + "description": "количество категорий в наборе" + } + ] + } + }, + "response": [] + }, + { + "name": "Получение информации о категории по её идентификатору", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " pm.collectionVariables.set(\"response\", category)\r", + " pm.collectionVariables.set(\"catid\", category.id)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Категория должна содержать поля: id, name\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('name');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.id).equal(target.id, 'Идентификатор категории должен соответствовать идентификатору в запросе');\r", + " pm.expect(source.name).equal(target.name, 'Название категории должно соответствовать названию категории с указанным идентификатором');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/categories/:catId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "categories", + ":catId" + ], + "variable": [ + { + "key": "catId", + "value": "{{catid}}", + "description": "(Required) id категории" + } + ] + } + }, + "response": [] + }, + { + "name": "Удаление категории", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const findedCategory = await api.findCategory(category.id);\r", + " pm.collectionVariables.set(\"catid\", category.id)\r", + " pm.collectionVariables.set(\"response\", findedCategory)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 204\", function () {\r", + " pm.response.to.have.status(204);\r", + "});\r", + "\r", + "source = pm.collectionVariables.get('response');\r", + "catId = pm.collectionVariables.get('catid');\r", + "\r", + "pm.test(\"Категория должна быть найдена до удаления\", function () {\r", + " pm.expect(source.id).equal(catId, 'Идентификтор категории должен совпадать с удаляемым');\r", + "});\r", + "\r", + "pm.sendRequest({\r", + " url: pm.collectionVariables.get(\"baseUrl\") + \"/categories/\" + catId,\r", + " method: 'GET',\r", + " }, (error, response) => {\r", + " pm.test(\"Категория не должна быть найдена после удаления\", function () {\r", + " pm.expect(response.code).to.eql(404);\r", + " });\r", + " });" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/categories/:catId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories", + ":catId" + ], + "variable": [ + { + "key": "catId", + "value": "{{catid}}" + } + ] + }, + "description": "Обратите внимание: с категорий не должно быть связано ни одного события." + }, + "response": [] + }, + { + "name": "Изменение категории", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + " let category\r", + " try {\r", + " category = await api.addCategory(rnd.getCategory());\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + " pm.collectionVariables.set(\"catid\", Number(category.id))\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " name : rnd.getCategory().name\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Категория должна содержать поля: id, name\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('name');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + " pm.expect(source.name).equal(target.name, 'Название категории должно совпадать с отправленным');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/categories/:catId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "categories", + ":catId" + ], + "variable": [ + { + "key": "catId", + "value": "{{catid}}" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Users", + "item": [ + { + "name": "Поиск пользователей", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Пользователи должны содержать поля: id, name, email\", function () {\r", + " pm.expect(target[0]).to.have.property('id');\r", + " pm.expect(target[0]).to.have.property('name');\r", + " pm.expect(target[0]).to.have.property('email');\r", + "});\r", + "\r", + "pm.test(\"Должен быть найден только один пользователь по заданному фильтру\", function () {\r", + " pm.expect(target.length).to.eql(1);\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target[0].id).equal(pm.collectionVariables.get(\"uid\"));\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/users?ids={{uid}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ], + "query": [ + { + "key": "ids", + "value": "{{uid}}", + "description": "id пользователей" + }, + { + "key": "ids", + "value": "-10833646", + "description": "id пользователей", + "disabled": true + }, + { + "key": "from", + "value": "0", + "description": "количество элементов, которые нужно пропустить для формирования текущего набора", + "disabled": true + }, + { + "key": "size", + "value": "10", + "description": "количество элементов в наборе", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Добавление нового пользователя", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Пользователь должен содержать поля: id, name, email\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('name');\r", + "pm.expect(target).to.have.property('email');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + " pm.expect(source.name).equal(target.name, 'Имя пользователя должно соответствовать отправленному в запросе');\r", + " pm.expect(source.email).equal(target.email, 'Почта пользователя должна соответствовать отправленной в запросе');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Удаление пользователя", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 204\", function () {\r", + " pm.response.to.have.status(204);\r", + "});\r", + "const source = pm.collectionVariables.get('response');\r", + "const userId = pm.collectionVariables.get('uid');\r", + "\r", + "pm.test(\"Пользователь должен быть найден до выполнения запроса\", function(){\r", + " pm.expect(source.length).to.eql(1);\r", + " pm.expect(source[0].id).to.eql(userId);\r", + "});\r", + "let body\r", + "const req = {\r", + " url: \"http://localhost:8080/admin/users?ids=\" + pm.collectionVariables.get(\"uid\"),\r", + " method: \"GET\",\r", + " body: body == null ? \"\" : JSON.stringify(body),\r", + " header: { \"Content-Type\": \"application/json\" },\r", + " };\r", + "pm.sendRequest(req, (error, response) => {\r", + " pm.test(\"Пользователь должен быть удалён после выполнения запроса\", function(){\r", + " pm.expect(response.json().length).to.eql(0);\r", + " });\r", + "})" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const foundedUser = await api.findUser(user.id);\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"response\", foundedUser)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/users/:userId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users", + ":userId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id пользователя" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Event", + "item": [ + { + "name": "Добавление нового события", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let event;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " event = rnd.getEvent(category.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(event),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201); \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, description, participantLimit, state, createdOn, location, requestModeration\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('annotation');\r", + "pm.expect(target).to.have.property('category');\r", + "pm.expect(target).to.have.property('paid');\r", + "pm.expect(target).to.have.property('eventDate');\r", + "pm.expect(target).to.have.property('initiator');\r", + "pm.expect(target).to.have.property('description');\r", + "pm.expect(target).to.have.property('participantLimit');\r", + "pm.expect(target).to.have.property('state');\r", + "pm.expect(target).to.have.property('createdOn');\r", + "pm.expect(target).to.have.property('location');\r", + "pm.expect(target).to.have.property('requestModeration');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + " pm.expect(target.title).equal(source.title, 'Название события должно соответствовать названию события в запросе');\r", + " pm.expect(target.annotation).equal(source.annotation, 'Аннотация события должна соответствовать аннотации события в запросе');\r", + " pm.expect(target.paid.toString()).equal(source.paid.toString(), 'Стоимость события должна соответствовать стоимости события в запросе');\r", + " pm.expect(target.eventDate).equal(source.eventDate, 'Дата проведения события должна соответствовать дате проведения события в запросе');\r", + " pm.expect(target.description).equal(source.description, 'Описание события должно соответствовать описание события в запросе');\r", + " pm.expect(target.participantLimit.toString()).equal(source.participantLimit.toString(), 'Лимит участников события должно соответствовать лимиту участников события в запросе');\r", + " pm.expect(target.location.lat.toString()).equal(source.location.lat.toString(), 'Широта локации проведения события должна соответствовать широте локации проведения события в запросе');\r", + " pm.expect(target.location.lon.toString()).equal(source.location.lon.toString(), 'Долгота локации проведения события должна соответствовать долготе локации проведения события в запросе');\r", + " pm.expect(target.requestModeration.toString()).equal(source.requestModeration.toString(), 'Необходимость модерации события должна соответствовать необходимости модерации события в запросе');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание: дата и время на которые намечено событие не может быть раньше, чем через два часа от текущего момента" + }, + "response": [] + }, + { + "name": "Добавление запроса от текущего пользователя на участие в событии", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " pm.request.removeQueryParams(['eventId']);\r", + " pm.request.addQueryParams([`eventId=` + event.id]);\r", + " pm.collectionVariables.set('uid', submittedUser.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201); \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "var query = {};\r", + "pm.request.url.query.all().forEach((param) => { query[param.key] = param.value});\r", + "\r", + "pm.test(\"Запрос на участие должен содержать поля: id, requester, event, status, created\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('requester');\r", + "pm.expect(target).to.have.property('event');\r", + "pm.expect(target).to.have.property('status');\r", + "pm.expect(target).to.have.property('created');\r", + "});\r", + "\r", + "pm.test(\"При создании у запроса на участие должен быть статус PENDING\", function () {\r", + " pm.expect(target.status).equal(\"PENDING\");\r", + "});\r", + "\r", + "pm.test(\"Id ивента в запросе и в ответе должны совпадать\", function () {\r", + " pm.expect(target.event.toString()).equal(query['eventId'].toString());\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/requests?eventId=0", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "requests" + ], + "query": [ + { + "key": "eventId", + "value": "0", + "description": "(Required) id события" + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- нельзя добавить повторный запрос\n- инициатор события не может добавить запрос на участие в своём событии\n- нельзя участвовать в неопубликованном событии\n- если у события достигнут лимит запросов на участие - необходимо вернуть ошибку\n- если для события отключена пре-модерация запросов на участие, то запрос должен автоматически перейти в состояние подтвержденного" + }, + "response": [] + }, + { + "name": "Поиск событий", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " pm.request.removeQueryParams(['users', 'categories']);\r", + " pm.request.addQueryParams([`users=` + user.id, 'categories=' + category.id]);\r", + " pm.collectionVariables.set('response', event);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json()[0];\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, views, confirmedRequests, description, participantLimit, state, createdOn, publishedOn, location, requestModeration\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('annotation');\r", + "pm.expect(target).to.have.property('category');\r", + "pm.expect(target).to.have.property('paid');\r", + "pm.expect(target).to.have.property('eventDate');\r", + "pm.expect(target).to.have.property('initiator');\r", + "pm.expect(target).to.have.property('views');\r", + "pm.expect(target).to.have.property('confirmedRequests');\r", + "pm.expect(target).to.have.property('description');\r", + "pm.expect(target).to.have.property('participantLimit');\r", + "pm.expect(target).to.have.property('state');\r", + "pm.expect(target).to.have.property('createdOn');\r", + "pm.expect(target).to.have.property('publishedOn');\r", + "pm.expect(target).to.have.property('location');\r", + "pm.expect(target).to.have.property('requestModeration');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.annotation).equal(target.annotation, 'Аннотация события должна соответствовать искомому событию');\r", + " pm.expect(source.category.id).equal(target.category.id, 'Идентификатор категории должен соответствовать искомой категории');\r", + " pm.expect(source.paid.toString()).equal(target.paid.toString(), 'Стоимость посещения события должна соответствовать искомому событию');\r", + " pm.expect(source.eventDate).equal(target.eventDate, 'Дата проведения события должна соответствовать дате искомого события');\r", + " pm.expect(source.description).equal(target.description, 'Описание события должно соответствовать искомому событию');\r", + " pm.expect(source.title).equal(target.title, 'Название события должно соответствовать искомому событию');\r", + " pm.expect(source.participantLimit.toString()).equal(target.participantLimit.toString(), 'Число участников события должно соответствовать искомому событию');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/events?users=0&states=PUBLISHED&categories=0&rangeStart=2022-01-06%2013%3A30%3A38&rangeEnd=2097-09-06%2013%3A30%3A38&from=0&size=1000", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events" + ], + "query": [ + { + "key": "users", + "value": "0", + "description": "список id пользователей, чьи события нужно найти" + }, + { + "key": "states", + "value": "PUBLISHED", + "description": "список состояний в которых находятся искомые события" + }, + { + "key": "categories", + "value": "0", + "description": "список id категорий в которых будет вестись поиск" + }, + { + "key": "rangeStart", + "value": "2022-01-06%2013%3A30%3A38", + "description": "дата и время не раньше которых должно произойти событие" + }, + { + "key": "rangeEnd", + "value": "2097-09-06%2013%3A30%3A38", + "description": "дата и время не позже которых должно произойти событие" + }, + { + "key": "from", + "value": "0", + "description": "количество событий, которые нужно пропустить для формирования текущего набора" + }, + { + "key": "size", + "value": "1000", + "description": "количество событий в наборе" + } + ] + }, + "description": "Эндпоинт возвращает полную информацию обо всех событиях подходящих под переданные условия" + }, + "response": [] + }, + { + "name": "Получение событий, добавленных текущим пользователем", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json()[0];\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate\", function () {\r", + " pm.expect(target).to.contain.keys('id', 'title', 'annotation', 'category', 'paid', 'eventDate');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/events?from=0&size=1000", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events" + ], + "query": [ + { + "key": "from", + "value": "0", + "description": "количество элементов, которые нужно пропустить для формирования текущего набора" + }, + { + "key": "size", + "value": "1000", + "description": "количество элементов в наборе" + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + } + }, + "response": [] + }, + { + "name": "Получение событий с возможностью фильтрации", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " pm.request.removeQueryParams(['text', 'categories', 'paid']);\r", + " pm.request.addQueryParams([`text=` + event.annotation, 'categories=' + category.id, 'paid=' + event.paid]);\r", + " pm.collectionVariables.set('response', event);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json()[0];\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, views, confirmedRequests\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('annotation');\r", + "pm.expect(target).to.have.property('category');\r", + "pm.expect(target).to.have.property('paid');\r", + "pm.expect(target).to.have.property('eventDate');\r", + "pm.expect(target).to.have.property('initiator');\r", + "pm.expect(target).to.have.property('views');\r", + "pm.expect(target).to.have.property('confirmedRequests');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.annotation).equal(target.annotation, 'Аннотация события должна соответствовать аннотации события с указанным идентификатором');\r", + " pm.expect(source.category.id).equal(target.category.id, 'Категория события должна соответствовать категории события с указанным идентификатором');\r", + " pm.expect(source.paid.toString()).equal(target.paid.toString(), 'Стоимость события должна соответствовать стоимости события с указанным идентификатором');\r", + " pm.expect(source.eventDate).equal(target.eventDate, 'Дата проведения события должна соответствовать дате проведения события с указанным идентификатором');\r", + " pm.expect(source.title).equal(target.title, 'Название события должно соответствовать названию события с указанным идентификатором');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/events?text=0&categories=0&paid=true&rangeStart=2022-01-06%2013%3A30%3A38&rangeEnd=2097-09-06%2013%3A30%3A38&onlyAvailable=false&sort=EVENT_DATE&from=0&size=1000", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "events" + ], + "query": [ + { + "key": "text", + "value": "0", + "description": "текст для поиска в содержимом аннотации и подробном описании события" + }, + { + "key": "categories", + "value": "0", + "description": "список идентификаторов категорий в которых будет вестись поиск" + }, + { + "key": "paid", + "value": "true", + "description": "поиск только платных/бесплатных событий" + }, + { + "key": "rangeStart", + "value": "2022-01-06%2013%3A30%3A38", + "description": "дата и время не раньше которых должно произойти событие" + }, + { + "key": "rangeEnd", + "value": "2097-09-06%2013%3A30%3A38", + "description": "дата и время не позже которых должно произойти событие" + }, + { + "key": "onlyAvailable", + "value": "false", + "description": "только события у которых не исчерпан лимит запросов на участие" + }, + { + "key": "sort", + "value": "EVENT_DATE", + "description": "Вариант сортировки: по дате события или по количеству просмотров" + }, + { + "key": "from", + "value": "0", + "description": "количество событий, которые нужно пропустить для формирования текущего набора" + }, + { + "key": "size", + "value": "1000", + "description": "количество событий в наборе" + } + ] + }, + "description": "Обратите внимание: \n- это публичный эндпоинт, соответственно в выдаче должны быть только опубликованные события\n- текстовый поиск (по аннотации и подробному описанию) должен быть без учета регистра букв\n- если в запросе не указан диапазон дат [rangeStart-rangeEnd], то нужно выгружать события, которые произойдут позже текущей даты и времени\n- информация о каждом событии должна включать в себя количество просмотров и количество уже одобренных заявок на участие\n- информацию о том, что по этому эндпоинту был осуществлен и обработан запрос, нужно сохранить в сервисе статистики" + }, + "response": [] + }, + { + "name": "Получение подробной информации об опубликованном событии по его идентификатору", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.collectionVariables.set('response', event);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, views, confirmedRequests, description, participantLimit, state, createdOn, publishedOn, location, requestModeration\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('annotation');\r", + "pm.expect(target).to.have.property('category');\r", + "pm.expect(target).to.have.property('paid');\r", + "pm.expect(target).to.have.property('eventDate');\r", + "pm.expect(target).to.have.property('initiator');\r", + "pm.expect(target).to.have.property('views');\r", + "pm.expect(target).to.have.property('confirmedRequests');\r", + "pm.expect(target).to.have.property('description');\r", + "pm.expect(target).to.have.property('participantLimit');\r", + "pm.expect(target).to.have.property('state');\r", + "pm.expect(target).to.have.property('createdOn');\r", + "pm.expect(target).to.have.property('publishedOn');\r", + "pm.expect(target).to.have.property('location');\r", + "pm.expect(target).to.have.property('requestModeration');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.annotation).equal(target.annotation, 'Аннотация события должна соответствовать аннотации события с указанным идентификатором');\r", + " pm.expect(source.category.id).equal(target.category.id, 'Категория события должна соответствовать категории события с указанным идентификатором');\r", + " pm.expect(source.paid.toString()).equal(target.paid.toString(), 'Стоимость события должна соответствовать стоимости события с указанным идентификатором');\r", + " pm.expect(source.eventDate).equal(target.eventDate, 'Дата проведения события должна соответствовать дате проведения события с указанным идентификатором');\r", + " pm.expect(source.description).equal(target.description, 'Описание события должно соответствовать описанию события с указанным идентификатором');\r", + " pm.expect(source.title).equal(target.title, 'Название события должно соответствовать названию события с указанным идентификатором');\r", + " pm.expect(source.participantLimit.toString()).equal(target.participantLimit.toString(), 'Лимит участников события должен соответствовать лимиту участников события с указанным идентификатором');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/events/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "events", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n- событие должно быть опубликовано\n- информация о событии должна включать в себя количество просмотров и количество подтвержденных запросов\n- информацию о том, что по этому эндпоинту был осуществлен и обработан запрос, нужно сохранить в сервисе статистики" + }, + "response": [] + }, + { + "name": "Получение полной информации о событии добавленном текущим пользователем", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"uid\", user.id)\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, description, participantLimit, state, createdOn, location, requestModeration\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('annotation');\r", + "pm.expect(target).to.have.property('category');\r", + "pm.expect(target).to.have.property('paid');\r", + "pm.expect(target).to.have.property('eventDate');\r", + "pm.expect(target).to.have.property('initiator');\r", + "pm.expect(target).to.have.property('description');\r", + "pm.expect(target).to.have.property('participantLimit');\r", + "pm.expect(target).to.have.property('state');\r", + "pm.expect(target).to.have.property('createdOn');\r", + "pm.expect(target).to.have.property('location');\r", + "pm.expect(target).to.have.property('requestModeration');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + } + }, + "response": [] + }, + { + "name": "Получение информации о заявках текущего пользователя на участие в чужих событиях", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " const requestToJoin = await api.publishParticipationRequest(event.id, submittedUser.id);\r", + " pm.collectionVariables.set('response', requestToJoin);\r", + " pm.collectionVariables.set('uid', submittedUser.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json()[0];\r", + "\r", + "pm.test(\"Запрос на участие должен содержать поля: id, requester, event, status, created\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('requester');\r", + "pm.expect(target).to.have.property('event');\r", + "pm.expect(target).to.have.property('status');\r", + "pm.expect(target).to.have.property('created');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.id).equal(target.id, 'Идентификатор запроса на участие в событии должен соответствовать идентификатору запроса, созданного ранее');\r", + " pm.expect(source.requester).equal(target.requester, 'Пользователя, запрашивающий доступ на участие в событии, должен соответствовать указанному пользователю');\r", + " pm.expect(source.event).equal(target.event, 'Событие, доступ к которому запрашивает пользователь, должно соответствовать событию, доступ к которому пользователь запрашивал доступ ранее');\r", + " pm.expect(source.created).equal(target.created, 'Время создания запроса на участие в событии должно соответствовать времени создания запроса, созданного ранее указанным пользователем');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/requests", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "requests" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + } + }, + "response": [] + }, + { + "name": "Получение информации о запросах на участие в событии текущего пользователя", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " const requestToJoin = await api.publishParticipationRequest(event.id, submittedUser.id);\r", + " pm.collectionVariables.set('response', requestToJoin);\r", + " pm.collectionVariables.set('uid', user.id);\r", + " pm.collectionVariables.set('eid', event.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json()[0];\r", + "\r", + "pm.test(\"Запрос на участие должен содержать поля: id, requester, event, status, created\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('requester');\r", + "pm.expect(target).to.have.property('event');\r", + "pm.expect(target).to.have.property('status');\r", + "pm.expect(target).to.have.property('created');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.id).equal(target.id, 'Идентификатор запроса на участие в событии должен соответствовать идентификатору запроса на участие в событии указанного пользователя');\r", + " pm.expect(source.requester).equal(target.requester, 'Автор запроса на участие в событии должен соответствовать указанному пользователю');\r", + " pm.expect(source.event).equal(target.event, 'Событие в ответе должно соответствовать событию с запросом на участие от указанного пользователя');\r", + " pm.expect(source.created).equal(target.created, 'Время создания запроса на участие в событии должно соответствовать времени создания запроса на участие в событии указанного пользователя');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/requests", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "requests" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + } + }, + "response": [] + }, + { + "name": "Редактирование данных события и его статуса (отклонение/публикация).", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " let event2 = rnd.getEvent(category.id)\r", + " event2.stateAction = \"PUBLISH_EVENT\"\r", + " pm.collectionVariables.set('response', event2);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: event2,\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, description, participantLimit, state, createdOn, publishedOn, location, requestModeration\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('annotation');\r", + "pm.expect(target).to.have.property('category');\r", + "pm.expect(target).to.have.property('paid');\r", + "pm.expect(target).to.have.property('eventDate');\r", + "pm.expect(target).to.have.property('initiator');\r", + "pm.expect(target).to.have.property('description');\r", + "pm.expect(target).to.have.property('participantLimit');\r", + "pm.expect(target).to.have.property('state');\r", + "pm.expect(target).to.have.property('createdOn');\r", + "pm.expect(target).to.have.property('publishedOn');\r", + "pm.expect(target).to.have.property('location');\r", + "pm.expect(target).to.have.property('requestModeration');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.annotation).equal(target.annotation, 'Аннотация события должна соответствовать искомому событию');\r", + " pm.expect(source.paid.toString()).equal(target.paid.toString(), 'Стоимость события должна соответствовать искомому событию');\r", + " pm.expect(source.eventDate).equal(target.eventDate, 'Дата проведения события должна соответствовать искомому событию');\r", + " pm.expect(source.description).equal(target.description, 'Описание события должно соответствовать искомому событию');\r", + " pm.expect(source.title).equal(target.title, 'Название события должно соответствовать искомому событию');\r", + " pm.expect(source.participantLimit.toString()).equal(target.participantLimit.toString(), 'Лимит участников события должен соответствовать искомому событию');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + }, + "description": "Обратите внимание:\n - дата начала события должна быть не ранее чем за час от даты публикации.\n- событие должно быть в состоянии ожидания публикации" + }, + "response": [] + }, + { + "name": "Изменение события добавленного текущим пользователем", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " pm.collectionVariables.set(\"uid\", user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id);\r", + " pm.collectionVariables.set(\"response\", event);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " stateAction: \"CANCEL_REVIEW\"\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get(\"response\");\r", + "const target = pm.response.json();\r", + "\r", + "\r", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, description, participantLimit, state, createdOn, location, requestModeration\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('annotation');\r", + "pm.expect(target).to.have.property('category');\r", + "pm.expect(target).to.have.property('paid');\r", + "pm.expect(target).to.have.property('eventDate');\r", + "pm.expect(target).to.have.property('initiator');\r", + "pm.expect(target).to.have.property('description');\r", + "pm.expect(target).to.have.property('participantLimit');\r", + "pm.expect(target).to.have.property('state');\r", + "pm.expect(target).to.have.property('createdOn');\r", + "pm.expect(target).to.have.property('location');\r", + "pm.expect(target).to.have.property('requestModeration');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.annotation).equal(target.annotation, 'Аннотация отменённого события должна соответствовать аннотации события до отмены');\r", + " pm.expect(source.category.id).equal(target.category.id, 'Категория отменённого события должна соответствовать категории события до отмены');\r", + " pm.expect(source.paid.toString()).equal(target.paid.toString(), 'Стоимость отменённого события должна соответствовать стоимости события до отмены');\r", + " pm.expect(source.eventDate).equal(target.eventDate, 'Дата проведения отменённого события должна соответствовать дате проведения события до отмены');\r", + " pm.expect(source.description).equal(target.description, 'Описание отменённого события должно соответствовать описанию события до отмены');\r", + " pm.expect(source.title).equal(target.title, 'Название отменённого события должно соответствовать названию события до отмены');\r", + " pm.expect(source.participantLimit.toString()).equal(target.participantLimit.toString(), 'Лимит участников отменённого события должен соответствовать лимиту участников события до отмены');\r", + " pm.expect(source.paid.toString()).equal(target.paid.toString(), 'Стоимость отменённого события должна соответствовать стоимости события до отмены');\r", + "});\r", + "\r", + "pm.test(\"Событие должно иметь статус PENDING при создании и статус CANCELED после выполнения запроса\", function () {\r", + " pm.expect(source.state).equal(\"PENDING\");\r", + " pm.expect(target.state).equal(\"CANCELED\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id отменяемого события" + } + ] + }, + "description": "Обратите внимание: Отменить можно только событие в состоянии ожидания модерации." + }, + "response": [] + }, + { + "name": "Отмена своего запроса на участие в событии", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " const requestToJoin = await api.publishParticipationRequest(event.id, submittedUser.id);\r", + " pm.collectionVariables.set('response', requestToJoin);\r", + " pm.collectionVariables.set('uid', submittedUser.id);\r", + " pm.collectionVariables.set('reqid', requestToJoin.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "const source = pm.collectionVariables.get('response');\r", + "\r", + "pm.test(\"Запрос на участие должен содержать поля: id, requester, event, status, created\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('requester');\r", + "pm.expect(target).to.have.property('event');\r", + "pm.expect(target).to.have.property('status');\r", + "pm.expect(target).to.have.property('created');\r", + "});\r", + "\r", + "pm.test(\"При создании у запроса на участие должен быть статус PENDING, а при удалении - CANCELED\", function () {\r", + " pm.expect(source.status).equal(\"PENDING\");\r", + " pm.expect(target.status).equal(\"CANCELED\");\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.id).equal(target.id, 'Идентификатор отменённого запроса на участие в событии должен соответствовать идентификатору запроса до отмены');\r", + " pm.expect(source.requester).equal(target.requester, 'Пользователь, отменяющий запрос на участие в событии, должен соответствовать текущему пользователю');\r", + " pm.expect(source.event).equal(target.event, 'Событие отменённого запроса на участие должно соответствовать запросу на участие в событии до отмены');\r", + " pm.expect(source.created).equal(target.created, 'Дата создания отменённого запроса на участие в событии должна соответствовать дате создания запроса до отмены');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/users/:userId/requests/:requestId/cancel", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "requests", + ":requestId", + "cancel" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "requestId", + "value": "{{reqid}}", + "description": "(Required) id запроса на участие" + } + ] + } + }, + "response": [] + }, + { + "name": "Отклонение запроса на участие в событии", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " const requestToJoin = await api.publishParticipationRequest(event.id, submittedUser.id);\r", + " pm.collectionVariables.set('response', requestToJoin);\r", + " pm.collectionVariables.set('uid', user.id);\r", + " pm.collectionVariables.set('eid', event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({requestIds: [requestToJoin.id],\r", + " status:\"REJECTED\"}),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json()[\"rejectedRequests\"][0];\r", + "\r", + "pm.test(\"Запрос на участие должен содержать поля: id, requester, event, status, created\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('requester');\r", + "pm.expect(target).to.have.property('event');\r", + "pm.expect(target).to.have.property('status');\r", + "pm.expect(target).to.have.property('created');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.id).equal(target.id, 'Идентификатор запроса на участие в событии должен соответствовать идентификатору запроса на участие в событии указанного пользователя');\r", + " pm.expect(source.requester).equal(target.requester, 'Пользователь, запрашивающий доступ на участие в событии должен пользователю, отправившему запрос на участие в событии указанного пользователя ранее');\r", + " pm.expect(source.event).equal(target.event, 'Событие, запрос на участие в котором надо подтвердить, должно соответствовать событию указанного пользователя');\r", + " pm.expect(source.created).equal(target.created, 'Время создания запроса на участие в событии после подтверждения должно соответствовать времени создания запроса на участие в событии указанного пользователя до подтверждения');\r", + "});\r", + "\r", + "pm.test(\"Запрос на участие должен иметь статус PENDING при создании и статус REJECTED после выполнения запроса\", function () {\r", + " pm.expect(source.status).equal(\"PENDING\");\r", + " pm.expect(target.status).equal(\"REJECTED\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"requestIds\": [\n 1,\n 2,\n 3\n ],\n \"status\": \"CONFIRMED\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/requests", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "requests" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- если для события лимит заявок равен 0 или отключена пре-модерация заявок, то подтверждение заявок не требуется\n- нельзя подтвердить заявку, если уже достигнут лимит по заявкам на данное событие\n- статус можно изменить только у заявок, находящихся в состоянии ожидания\n- если при подтверждении данной заявки, лимит заявок для события исчерпан, то все неподтверждённые заявки необходимо отклонить" + }, + "response": [] + }, + { + "name": "Изменение статуса (подтверждена, отменена) заявок на участие в событии текущего пользователя", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " const requestToJoin = await api.publishParticipationRequest(event.id, submittedUser.id);\r", + " pm.collectionVariables.set('response', requestToJoin);\r", + " pm.collectionVariables.set('uid', user.id);\r", + " pm.collectionVariables.set('eid', event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({requestIds: [requestToJoin.id],\r", + " status:\"CONFIRMED\"}),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json()[\"confirmedRequests\"][0];\r", + "\r", + "pm.test(\"Запрос на участие должен содержать поля: id, requester, event, status, created\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('requester');\r", + "pm.expect(target).to.have.property('event');\r", + "pm.expect(target).to.have.property('status');\r", + "pm.expect(target).to.have.property('created');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.id).equal(target.id, 'Идентификатор запроса на участие в событии должен соответствовать идентификатору запроса на участие в событии указанного пользователя');\r", + " pm.expect(source.requester).equal(target.requester, 'Пользователь, запрашивающий доступ на участие в событии должен пользователю, отправившему запрос на участие в событии указанного пользователя ранее');\r", + " pm.expect(source.event).equal(target.event, 'Событие, запрос на участие в котором надо подтвердить, должно соответствовать событию указанного пользователя');\r", + " pm.expect(source.created).equal(target.created, 'Время создания запроса на участие в событии после подтверждения должно соответствовать времени создания запроса на участие в событии указанного пользователя до подтверждения');\r", + "});\r", + "\r", + "pm.test(\"Запрос на участие должен иметь статус PENDING при создании и статус CONFIRMED после выполнения запроса\", function () {\r", + " pm.expect(source.status).equal(\"PENDING\");\r", + " pm.expect(target.status).equal(\"CONFIRMED\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/requests", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "requests" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события текущего пользователя" + } + ] + }, + "description": "Обратите внимание:\n- если для события лимит заявок равен 0 или отключена пре-модерация заявок, то подтверждение заявок не требуется\n- нельзя подтвердить заявку, если уже достигнут лимит по заявкам на данное событие\n- статус можно изменить только у заявок, находящихся в состоянии ожидания\n- если при подтверждении данной заявки, лимит заявок для события исчерпан, то все неподтверждённые заявки необходимо отклонить" + }, + "response": [] + } + ] + }, + { + "name": "Compilation", + "item": [ + { + "name": "Добавление новой подборки", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let compilation;\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " compilation = rnd.getCompilation(event.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(compilation),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Подборка должны содержать поля: id, title, pinned, events\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('pinned');\r", + "pm.expect(target).to.have.property('events');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).to.not.be.null;\r", + " pm.expect(target.title).to.be.a(\"string\");\r", + " pm.expect(target.events).to.be.an(\"array\");\r", + "\r", + " pm.expect(source.events[0]).equal(target.events[0].id, 'Идентификаторы событий в подборке должен быть идентичен идентификаторам, указанным при создании подборки ');\r", + " pm.expect(source.title).equal(target.title, 'Название подборки должно соответствовать указанному при создании');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations" + ] + } + }, + "response": [] + }, + { + "name": "Получение подборки событий по её id", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const compilation = await api.addCompilation(rnd.getCompilation());\r", + " pm.collectionVariables.set('response', compilation);\r", + " pm.collectionVariables.set('compid', compilation.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Подборка должны содержать поля: id, title, pinned, events\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('title');\r", + "pm.expect(target).to.have.property('pinned');\r", + "pm.expect(target).to.have.property('events');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.id).equal(target.id, 'Идентификатор подборки должен соответствовать идентификатору подборки добавленной ранее');\r", + " pm.expect(source.title).equal(target.title, 'Название подборки должно соответствовать названию подборки добавленной ранее');\r", + " pm.expect(source.pinned).equal(target.pinned, 'Закреплённость подборки должна соответствовать закреплённости подборки добавленной ранее');\r", + " pm.expect(source.events.join()).equal(target.events.join(), 'События подборки должны соответствовать событиям подборки добавленной ранее');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/compilations/:compId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "compilations", + ":compId" + ], + "variable": [ + { + "key": "compId", + "value": "{{compid}}", + "description": "(Required) id подборки" + } + ] + } + }, + "response": [] + }, + { + "name": "Получение подборок событий", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let newComp = rnd.getCompilation();\r", + " newComp['pinned'] = true;\r", + " const compilation = await api.addCompilation(newComp);\r", + " pm.collectionVariables.set('response', compilation);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get('response');\r", + "const target = pm.response.json();\r", + "let founded;\r", + "target.forEach(function(element){if (element.id == source.id) founded = element});\r", + "\r", + "pm.test(\"Подборка должны содержать поля: id, title, pinned, events\", function () {\r", + "pm.expect(target[0]).to.have.property('id');\r", + "pm.expect(target[0]).to.have.property('title');\r", + "pm.expect(target[0]).to.have.property('pinned');\r", + "pm.expect(target[0]).to.have.property('events');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(source.id).equal(founded.id, 'Идентификатор подборки должен соответствовать идентификатору подборки добавленной ранее');\r", + " pm.expect(source.title).equal(founded.title, 'Название подборки должно соответствовать названию подборки добавленной ранее');\r", + " pm.expect(source.pinned).equal(founded.pinned, 'Закрепленность подборки должна соответствовать закрепленности подборки добавленной ранее');\r", + " pm.expect(source.events.join()).equal(founded.events.join(), 'События подборки должны соответствовать событиям подборки добавленной ранее');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/compilations?pinned=true&from=0&size=1000", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "compilations" + ], + "query": [ + { + "key": "pinned", + "value": "true", + "description": "искать только закрепленные/не закрепленные подборки" + }, + { + "key": "from", + "value": "0", + "description": "количество элементов, которые нужно пропустить для формирования текущего набора" + }, + { + "key": "size", + "value": "1000", + "description": "количество элементов в наборе" + } + ] + } + }, + "response": [] + }, + { + "name": "Удаление подборки", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 204\", function () {\r", + " pm.response.to.have.status(204);\r", + "});\r", + "\r", + "let source = pm.collectionVariables.get('response');\r", + "\r", + "pm.test(\"Подборка должна быть найдена до удаления\", function () {\r", + " pm.expect(source).not.to.be.null;\r", + "});\r", + "\r", + "let body\r", + "const req = {\r", + " url: \"http://localhost:8080/compilations?from=0&size=1000\" + pm.collectionVariables.get(\"uid\"),\r", + " method: \"GET\",\r", + " body: body == null ? \"\" : JSON.stringify(body),\r", + " header: { \"Content-Type\": \"application/json\" },\r", + " };\r", + "pm.sendRequest(req, (error, response) => {\r", + " pm.test(\"Подборка должна быть удалена после выполнения запроса\", function(){\r", + " response.json().forEach(element => {\r", + " if(element.id == pm.collectionVariables.get('compid')){\r", + " throw new Error(\"Подборка все еще находится в списке существующих\");\r", + " }\r", + " })\r", + " });\r", + "})\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " const compilation = await api.addCompilation(rnd.getCompilation());\r", + " const foundedCompilation = await api.findCompilation(compilation.id);\r", + " pm.collectionVariables.set('compid', compilation.id);\r", + " pm.collectionVariables.set('response', foundedCompilation);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/admin/compilations/:compId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations", + ":compId" + ], + "variable": [ + { + "key": "compId", + "value": "{{compid}}", + "description": "(Required) id подборки" + } + ] + } + }, + "response": [] + }, + { + "name": "Обновить информацию о подборке", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " const compilation = await api.addCompilation(rnd.getCompilation());\r", + " const foundedCompilation = await api.findCompilation(compilation.id);\r", + " pm.collectionVariables.set('compid', compilation.id);\r", + " pm.collectionVariables.set('response', foundedCompilation);\r", + " pm.collectionVariables.set('eid', event.id);\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " events : [event.id],\r", + " pinned: true,\r", + " title: rnd.getCompilation().name\r", + " }),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200\", function () {\r", + " pm.response.to.be.ok; \r", + "});\r", + "\r", + "source = pm.collectionVariables.get('response');\r", + "compId = pm.collectionVariables.get('compid');\r", + "eventId = pm.collectionVariables.get('eid');\r", + "\r", + "pm.test(\"Событие не должно быть найдено в подборке до добавления\", function () {\r", + " pm.expect(source.events.length).equal(0);\r", + "});\r", + "\r", + "pm.sendRequest({\r", + " url: pm.collectionVariables.get(\"baseUrl\") + \"/compilations/\" + compId,\r", + " method: 'GET',\r", + " }, (error, response) => {\r", + " \r", + " pm.test(\"Событие должно быть найдено в подборке после добавления\", function () {\r", + " pm.expect(response.json().events[0].id).equal(eventId);\r", + " });\r", + " });" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/admin/compilations/:compId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "compilations", + ":compId" + ], + "variable": [ + { + "key": "compId", + "value": "{{compid}}" + } + ] + } + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "API = class {", + " constructor(postman, verbose = false, baseUrl = \"http://localhost:8080\") {", + " this.baseUrl = baseUrl;", + " this.pm = postman;", + " this._verbose = verbose;", + " }", + "", + " async addUser(user, verbose=null) {", + " return this.post(\"/admin/users\", user, \"Ошибка при добавлении нового пользователя: \", verbose);", + " }", + "", + " async addCategory(category, verbose=null) {", + " return this.post(\"/admin/categories\", category, \"Ошибка при добавлении новой категории: \", verbose);", + " }", + "", + " async addEvent(userId, event, verbose=null) {", + " return this.post(\"/users/\" + userId + \"/events\", event, \"Ошибка при добавлении нового события: \", verbose);", + " }", + "", + " async addCompilation(compilation, verbose=null) {", + " return this.post(\"/admin/compilations\", compilation, \"Ошибка при добавлении новой подборки: \", verbose);", + " }", + "", + " async publishParticipationRequest(eventId, userId, verbose=null) {", + " return this.post('/users/' + userId + '/requests?eventId=' + eventId, null, \"Ошибка при добавлении нового запроса на участие в событии\", verbose);", + " }", + "", + " async publishEvent(eventId, verbose=null) {", + " return this.patch('/admin/events/' + eventId, {stateAction: \"PUBLISH_EVENT\"}, \"Ошибка при публикации события\", verbose);", + " }", + " ", + " async rejectEvent(eventId, verbose=null) {", + " return this.patch('/admin/events/' + eventId, {stateAction: \"REJECT_EVENT\"}, \"Ошибка при отмене события\", verbose);", + " }", + "", + " async acceptParticipationRequest(eventId, userId, reqId, verbose=null) {", + " return this.patch('/users/' + userId + '/events/' + eventId + '/requests', {requestIds:[reqId], status: \"CONFIRMED\"}, \"Ошибка при принятии заявки на участие в событии\", verbose);", + " }", + "", + " async findCategory(catId, verbose=null) {", + " return this.get('/categories/' + catId, null, \"Ошибка при поиске категории по id\", verbose);", + " }", + "", + " async findCompilation(compId, verbose=null) {", + " return this.get('/compilations/' + compId, null, \"Ошибка при поиске подборки по id\", verbose);", + " }", + "", + " async findEvent(eventId, verbose=null) {", + " return this.get('/events/' + eventId, null, \"Ошибка при поиске события по id\", verbose);", + " }", + "", + " async findUser(userId, verbose=null) {", + " return this.get('/admin/users?ids=' + userId, null, \"Ошибка при поиске пользователя по id\", verbose);", + " }", + "", + " async post(path, body, errorText = \"Ошибка при выполнении post-запроса: \", verbose=null) {", + " return this.sendRequest(\"POST\", path, body, errorText, verbose);", + " }", + "", + " async patch(path, body = null, errorText = \"Ошибка при выполнении patch-запроса: \", verbose=null) {", + " return this.sendRequest(\"PATCH\", path, body, errorText, verbose);", + " }", + "", + " async get(path, body = null, errorText = \"Ошибка при выполнении get-запроса: \", verbose=null) {", + " return this.sendRequest(\"GET\", path, body, errorText, verbose);", + " }", + " async sendRequest(method, path, body=null, errorText = \"Ошибка при выполнении запроса: \", verbose=null) {", + " return new Promise((resolve, reject) => {", + " verbose = verbose == null ? this._verbose : verbose;", + " const request = {", + " url: this.baseUrl + path,", + " method: method,", + " body: body == null ? \"\" : JSON.stringify(body),", + " header: { \"Content-Type\": \"application/json\" },", + " };", + " if(verbose) {", + " console.log(\"Отправляю запрос: \", request);", + " }", + "", + " try {", + " this.pm.sendRequest(request, (error, response) => {", + " if(error || (response.code >= 400 && response.code <= 599)) {", + " let err = error ? error : JSON.stringify(response.json());", + " console.error(\"При выполнении запроса к серверу возникла ошика.\\n\", err,", + " \"\\nДля отладки проблемы повторите такой же запрос к вашей программе \" + ", + " \"на локальном компьютере. Данные запроса:\\n\", JSON.stringify(request));", + "", + " reject(new Error(errorText + err));", + " }", + " if(verbose) {", + " console.log(\"Результат обработки запроса: код состояния - \", response.code, \", тело: \", response.json());", + " }", + " if (response.stream.length === 0){", + " reject(new Error('Отправлено пустое тело ответа'))", + " }else{", + " resolve(response.json());", + " }", + " });", + " ", + " } catch(err) {", + " if(verbose) {", + " console.error(errorText, err);", + " }", + " return Promise.reject(err);", + " }", + " });", + " }", + "};", + "", + "RandomUtils = class {", + " constructor() {}", + "", + " getUser() {", + " return {", + " name: pm.variables.replaceIn('{{$randomFullName}}'),", + " email: pm.variables.replaceIn('{{$randomEmail}}')", + " };", + " }", + "", + " getCategory() {", + " return {", + " name: pm.variables.replaceIn('{{$randomWord}}') + Math.floor(Math.random() * 10000 * Math.random()).toString()", + " };", + " }", + "", + " getEvent(categoryId) {", + " return {", + " annotation: pm.variables.replaceIn('{{$randomLoremParagraph}}'),", + " category: categoryId,", + " description: pm.variables.replaceIn('{{$randomLoremParagraphs}}'),", + " eventDate: this.getFutureDateTime(),", + " location: {", + " lat: parseFloat(pm.variables.replaceIn('{{$randomLatitude}}')),", + " lon: parseFloat(pm.variables.replaceIn('{{$randomLongitude}}')),", + " },", + " paid: pm.variables.replaceIn('{{$randomBoolean}}'),", + " participantLimit: pm.variables.replaceIn('{{$randomInt}}'),", + " requestModeration: pm.variables.replaceIn('{{$randomBoolean}}'),", + " title: pm.variables.replaceIn('{{$randomLoremSentence}}'),", + " }", + " }", + "", + " getCompilation(...eventIds) {", + " return {", + " title: pm.variables.replaceIn('{{$randomLoremSentence}}').slice(0, 50),", + " pinned: pm.variables.replaceIn('{{$randomBoolean}}'),", + " events: eventIds", + " };", + " }", + "", + "", + " getFutureDateTime(hourShift = 5, minuteShift=0, yearShift=0) {", + " let moment = require('moment');", + "", + " let m = moment();", + " m.add(hourShift, 'hour');", + " m.add(minuteShift, 'minute');", + " m.add(yearShift, 'year');", + "", + " return m.format('YYYY-MM-DD HH:mm:ss');", + " }", + "", + " getWord(length = 1) {", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const charactersLength = characters.length;", + " let counter = 0;", + " while (counter < length) {", + " result += characters.charAt(Math.floor(Math.random() * charactersLength));", + " counter += 1;", + " }", + " return result;", + " }", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "http://localhost:8080", + "type": "string" + }, + { + "key": "name", + "value": "" + }, + { + "key": "usersCount", + "value": 1, + "type": "number" + }, + { + "key": "catid", + "value": 1, + "type": "number" + }, + { + "key": "request_body", + "value": "" + }, + { + "key": "mail", + "value": "" + }, + { + "key": "response", + "value": "" + }, + { + "key": "uid", + "value": 1, + "type": "number" + }, + { + "key": "catname", + "value": "" + }, + { + "key": "eid", + "value": 1, + "type": "number" + }, + { + "key": "compid", + "value": 1, + "type": "number" + }, + { + "key": "toCheck", + "value": "" + }, + { + "key": "newDataToSet", + "value": "" + }, + { + "key": "uid1", + "value": "" + }, + { + "key": "reqid", + "value": 1, + "type": "number" + }, + { + "key": "catId", + "value": "" + }, + { + "key": "confirmedRequests", + "value": "" + }, + { + "key": "responseArr", + "value": "" + }, + { + "key": "source1", + "value": "" + }, + { + "key": "source2", + "value": "" + }, + { + "key": "fromId", + "value": "0" + }, + { + "key": "source", + "value": "" + } + ] +} \ No newline at end of file diff --git a/Postman/ewm-stat-service.json b/Postman/ewm-stat-service.json new file mode 100644 index 0000000..25dca47 --- /dev/null +++ b/Postman/ewm-stat-service.json @@ -0,0 +1,1027 @@ +{ + "info": { + "_postman_id": "1b12f39d-a514-42d9-99a1-2d2f4026b8e8", + "name": "\"Explore with me\" API статистика", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "23073145", + "_collection_link": "https://universal-shadow-295426.postman.co/workspace/My-Workspace~4200f6aa-0504-44b1-8a1d-707d0dcbd5ce/collection/23073145-1b12f39d-a514-42d9-99a1-2d2f4026b8e8?action=share&creator=23073145&source=collection_link" + }, + "item": [ + { + "name": "Сохранение информации о том, что к эндпоинту был запрос", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let post;\r", + " try {\r", + " post = rnd.getPost();\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(post),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/hit", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "hit" + ] + }, + "description": "Сохранение информации о том, что на uri конкретного сервиса был отправлен запрос пользователем. Название сервиса, uri и ip пользователя указаны в теле запроса." + }, + "response": [] + }, + { + "name": "Получение статистики по посещениям. Обратите внимание: значение даты и времени нужно закодировать (например используя java.net.URLEncoder.encode)(Тест на /events/{id})", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try { \r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " pm.collectionVariables.set(\"uri\", '/events/' + event.id);\r", + "\r", + " pm.sendRequest({\r", + " url : \"http://localhost:8080/events/\" + event.id,\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " pm.sendRequest({\r", + " url : \"http://localhost:9090/stats?start=2020-05-05 00:00:00&end=2035-05-05 00:00:00&uris=/events/\" + event.id.toString() + \"&unique=false\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " pm.collectionVariables.set('source', response.json());\r", + " pm.sendRequest({\r", + " url : \"http://localhost:8080/events/\" + event.id,\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " });\r", + " });\r", + " });\r", + " \r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json()[0];\r", + "\r", + "pm.test(\"Посты должны содержать поля: app, uri, hits\", function () {\r", + " pm.expect(target).to.have.all.keys('app', 'uri', 'hits');\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get(\"source\")[0];\r", + "\r", + "\r", + "pm.test(\"После выполнения запроса GET /events/{id} должно увеличиться количество хитов.\", function(){\r", + " pm.expect(source.hits + 1).equal(target.hits, 'Количество хитов после выполнения запроса GET /events/{id} должно быть больше на 1.');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/stats?start=2020-05-05 00:00:00&end=2035-05-05 00:00:00&uris={{uri}}&unique=false", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "stats" + ], + "query": [ + { + "key": "start", + "value": "2020-05-05 00:00:00", + "description": "(Required) Дата и время начала диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "end", + "value": "2035-05-05 00:00:00", + "description": "(Required) Дата и время конца диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "uris", + "value": "{{uri}}", + "description": "Список uri для которых нужно выгрузить статистику" + }, + { + "key": "unique", + "value": "false", + "description": "Нужно ли учитывать только уникальные посещения (только с уникальным ip)" + } + ] + } + }, + "response": [] + }, + { + "name": "Тест взаимодействия сервисов", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try { \r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event1 = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event1 = await api.publishEvent(event1.id);\r", + " let event2 = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event2 = await api.publishEvent(event2.id);\r", + " pm.collectionVariables.set(\"uri\", '/events/' + event1.id + '&uris=/events/' + event2.id);\r", + "\r", + " pm.sendRequest({\r", + " url : \"http://localhost:8080/events/\" + event1.id,\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " pm.sendRequest({\r", + " url : \"http://localhost:8080/events/\" + event2.id,\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " pm.sendRequest({\r", + " url : \"http://localhost:8080/events/\" + event2.id,\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " });\r", + " });\r", + " });\r", + " \r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Посты должны содержать поля: app, uri, hits\", function () {\r", + " pm.expect(target[0]).to.have.all.keys('app', 'uri', 'hits');\r", + " pm.expect(target[1]).to.have.all.keys('app', 'uri', 'hits');\r", + "});\r", + "\r", + "pm.test(\"В теле ответа должна соблюдаться сортировка по убыванию количества просмотров\", function(){\r", + " pm.expect(target[0].hits).to.be.above(target[1].hits);\r", + "});\r", + "\r", + "pm.test(\"Проверка соответствия реального количества просмотров событий и сохраненных хитов\", function(){\r", + " pm.expect(target[0].hits).equal(2);\r", + " pm.expect(target[1].hits).equal(1);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/stats?start=2020-05-05 00:00:00&end=2035-05-05 00:00:00&uris={{uri}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "stats" + ], + "query": [ + { + "key": "start", + "value": "2020-05-05 00:00:00", + "description": "(Required) Дата и время начала диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "end", + "value": "2035-05-05 00:00:00", + "description": "(Required) Дата и время конца диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "uris", + "value": "{{uri}}", + "description": "Список uri для которых нужно выгрузить статистику" + }, + { + "key": "uris", + "value": "aliqua o", + "description": "Список uri для которых нужно выгрузить статистику", + "disabled": true + }, + { + "key": "unique", + "value": "false", + "description": "Нужно ли учитывать только уникальные посещения (только с уникальным ip)", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Получение статистики по посещениям. Обратите внимание: значение даты и времени нужно закодировать (например используя java.net.URLEncoder.encode)(Тест на /events)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try { \r", + " pm.collectionVariables.set(\"uri\", '/events'); \r", + " \r", + " pm.sendRequest({\r", + " url : \"http://localhost:8080/events\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " \r", + " pm.sendRequest({\r", + " url : \"http://localhost:9090/stats?start=2020-05-05 00:00:00&end=2035-05-05 00:00:00&uris=/events&unique=false\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " pm.collectionVariables.set('source', response.json());\r", + " pm.sendRequest({\r", + " url : \"http://localhost:8080/events\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " });\r", + " });\r", + " });\r", + " \r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json()[0];\r", + "\r", + "pm.test(\"Посты должны содержать поля: app, uri, hits\", function () {\r", + " pm.expect(target).to.have.all.keys('app', 'uri', 'hits');\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get(\"source\")[0];\r", + "\r", + "\r", + "pm.test(\"После выполнения запроса GET /events должно увеличиться количество хитов.\", function(){\r", + " pm.expect(source.hits + 1).equal(target.hits, 'Количество хитов после выполнения запроса GET /events должно быть больше на 1.');\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/stats?start=2020-05-05 00:00:00&end=2035-05-05 00:00:00&uris={{uri}}&unique=false", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "stats" + ], + "query": [ + { + "key": "start", + "value": "2020-05-05 00:00:00", + "description": "(Required) Дата и время начала диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "end", + "value": "2035-05-05 00:00:00", + "description": "(Required) Дата и время конца диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "uris", + "value": "{{uri}}", + "description": "Список uri для которых нужно выгрузить статистику" + }, + { + "key": "unique", + "value": "false", + "description": "Нужно ли учитывать только уникальные посещения (только с уникальным ip)" + } + ] + } + }, + "response": [] + }, + { + "name": "Получение статистики по посещениям. (Тест на опциональность и работу параметра unique)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try { \r", + " pm.collectionVariables.set(\"uri\", '/events'); \r", + " \r", + " pm.sendRequest({\r", + " url : \"http://localhost:8080/events\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " pm.sendRequest({\r", + " url : \"http://localhost:8080/events\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " \r", + " pm.sendRequest({\r", + " url : \"http://localhost:9090/stats?start=2020-05-05 00:00:00&end=2035-05-05 00:00:00&uris=/events&unique=true\",\r", + " method : \"GET\",\r", + " header: { \"Content-Type\": \"application/json\" }\r", + " }, (error, response) => {\r", + " pm.collectionVariables.set('source', response.json());\r", + " });\r", + " });\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json при запросе без опционального параметра unique\", function () {\r", + " pm.response.to.be.ok; \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const target = pm.response.json()[0];\r", + "\r", + "pm.test(\"Посты должны содержать поля: app, uri, hits\", function () {\r", + " pm.expect(target).to.have.all.keys('app', 'uri', 'hits');\r", + "});\r", + "\r", + "const source = pm.collectionVariables.get(\"source\")[0];\r", + "\r", + "pm.test(\"Количество уникальных просмотров с одного ip должно равняться 1\", function () {\r", + " pm.expect(source.hits == 1);\r", + "})\r", + "\r", + "pm.test(\"Количество просмотров с одного ip должно быть больше 1\", function () {\r", + " pm.expect(target.hits > 1);\r", + "})" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/stats?start=2020-05-05 00:00:00&end=2035-05-05 00:00:00&uris={{uri}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "stats" + ], + "query": [ + { + "key": "start", + "value": "2020-05-05 00:00:00", + "description": "(Required) Дата и время начала диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "end", + "value": "2035-05-05 00:00:00", + "description": "(Required) Дата и время конца диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "uris", + "value": "{{uri}}", + "description": "Список uri для которых нужно выгрузить статистику" + }, + { + "key": "uris", + "value": "aliqua o", + "description": "Список uri для которых нужно выгрузить статистику", + "disabled": true + }, + { + "key": "unique", + "value": "false", + "description": "Нужно ли учитывать только уникальные посещения (только с уникальным ip)", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Получение статистики по посещениям. (Тест на верную обработку запроса с неверными датами начала и конца диапазона времени)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400\", function () {\r", + " pm.response.to.be.badRequest;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/stats?start=2035-05-05 00:00:00&end=2020-05-05 00:00:00&uris={{uri}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "stats" + ], + "query": [ + { + "key": "start", + "value": "2035-05-05 00:00:00", + "description": "(Required) Дата и время начала диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "end", + "value": "2020-05-05 00:00:00", + "description": "(Required) Дата и время конца диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "uris", + "value": "{{uri}}", + "description": "Список uri для которых нужно выгрузить статистику" + }, + { + "key": "unique", + "value": "false", + "description": "Нужно ли учитывать только уникальные посещения (только с уникальным ip)", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Тест на верную обработку запроса без даты начала", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400\", function () {\r", + " pm.response.to.be.badRequest;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/stats?end=2020-05-05 00:00:00&uris={{uri}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "stats" + ], + "query": [ + { + "key": "start", + "value": "2035-05-05 00:00:00", + "description": "(Required) Дата и время начала диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")", + "disabled": true + }, + { + "key": "end", + "value": "2020-05-05 00:00:00", + "description": "(Required) Дата и время конца диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "uris", + "value": "{{uri}}", + "description": "Список uri для которых нужно выгрузить статистику" + }, + { + "key": "unique", + "value": "false", + "description": "Нужно ли учитывать только уникальные посещения (только с уникальным ip)", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Тест на верную обработку запроса без даты конца", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 400\", function () {\r", + " pm.response.to.be.badRequest;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/stats?start=2035-05-05 00:00:00&uris={{uri}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "stats" + ], + "query": [ + { + "key": "start", + "value": "2035-05-05 00:00:00", + "description": "(Required) Дата и время начала диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")" + }, + { + "key": "end", + "value": "2020-05-05 00:00:00", + "description": "(Required) Дата и время конца диапазона за который нужно выгрузить статистику (в формате \"yyyy-MM-dd HH:mm:ss\")", + "disabled": true + }, + { + "key": "uris", + "value": "{{uri}}", + "description": "Список uri для которых нужно выгрузить статистику" + }, + { + "key": "unique", + "value": "false", + "description": "Нужно ли учитывать только уникальные посещения (только с уникальным ip)", + "disabled": true + } + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "API = class {", + " constructor(postman, verbose = false, baseUrl = \"http://localhost:9090\") {", + " this.baseUrl = baseUrl;", + " this.pm = postman;", + " this._verbose = verbose;", + " }", + "", + " async addPost(post, verbose=null) {", + " return this.post(\"/hit\", post, \"Ошибка при сохранении информации о запросе к эндпойнту: \", verbose);", + " }", + "", + " async addUser(user, verbose=null) {", + " return this.post(\"/admin/users\", user, \"http://localhost:8080\", \"Ошибка при добавлении нового пользователя: \", verbose);", + " }", + "", + " async addCategory(category, verbose=null) {", + " return this.post(\"/admin/categories\", category, \"http://localhost:8080\", \"Ошибка при добавлении новой категории: \", verbose);", + " }", + "", + " async addEvent(userId, event, verbose=null) {", + " return this.post(\"/users/\" + userId + \"/events\", event, \"http://localhost:8080\", \"Ошибка при добавлении нового события: \", verbose);", + " }", + "", + " async publishEvent(eventId, verbose=null) {", + " return this.patch('/admin/events/' + eventId, {stateAction: \"PUBLISH_EVENT\"},\"Ошибка при публикации события\", verbose);", + " }", + "", + " async patch(path, body = null, errorText = \"Ошибка при выполнении patch-запроса: \", verbose=null) {", + " return this.sendRequest(\"PATCH\", path, \"http://localhost:8080\", body, errorText);", + " }", + "", + " async post(path, body, newBaseUrl=null, errorText = \"Ошибка при выполнении post-запроса: \", verbose=null) {", + " return this.sendRequest(\"POST\", path, newBaseUrl, body, errorText);", + " }", + "", + " async sendRequest(method, path, newBaseUrl=null, body=null, errorText = \"Ошибка при выполнении запроса: \", verbose=null) {", + " return new Promise((resolve, reject) => {", + " verbose = verbose == null ? this._verbose : verbose;", + " let request;", + " if (newBaseUrl==null)", + " request = {", + " url: this.baseUrl + path,", + " method: method,", + " body: body == null ? \"\" : JSON.stringify(body),", + " header: { \"Content-Type\": \"application/json\" },", + " };", + " else", + " request = {", + " url: newBaseUrl + path,", + " method: method,", + " body: body == null ? \"\" : JSON.stringify(body),", + " header: { \"Content-Type\": \"application/json\" },", + " };", + "", + " if(verbose) {", + " console.log(\"Отправляю запрос: \", request);", + " }", + "", + " try {", + " this.pm.sendRequest(request, (error, response) => {", + " if(error || (response.code >= 400 && response.code <= 599)) {", + " let err = error ? error : JSON.stringify(response.json());", + " console.error(\"При выполнении запроса к серверу возникла ошика.\\n\", err,", + " \"\\nДля отладки проблемы повторите такой же запрос к вашей программе \" + ", + " \"на локальном компьютере. Данные запроса:\\n\", JSON.stringify(request));", + "", + " reject(new Error(errorText + err));", + " }", + "", + " if(verbose) {", + " console.log(\"Результат обработки запроса: код состояния - \", response.code, \", тело: \", response.json());", + " }", + " try{", + " resolve(response.json());", + " } catch(err){", + " resolve(response);", + " }", + " ", + " });", + " } catch(err) {", + " if(verbose) {", + " console.error(errorText, err);", + " }", + " return Promise.reject(err);", + " }", + " });", + " }", + "};", + "", + "RandomUtils = class {", + " constructor() {}", + "", + " getPost() {", + " return {", + " app: \"ewm-main-service\",", + " uri: \"/events/\" + pm.variables.replaceIn('{{$randomInt}}'),", + " ip: pm.variables.replaceIn('{{$randomIP}}'),", + " timestamp: this.getPastDateTime()", + " }", + " }", + "", + " getUser() {", + " return {", + " name: pm.variables.replaceIn('{{$randomFullName}}'),", + " email: pm.variables.replaceIn('{{$randomEmail}}')", + " };", + " }", + "", + " getCategory() {", + " return {", + " name: pm.variables.replaceIn('{{$randomWord}}') + Math.floor(Math.random() * 100).toString()", + " };", + " }", + "", + " getEvent(categoryId) {", + " return {", + " annotation: pm.variables.replaceIn('{{$randomLoremParagraph}}'),", + " category: categoryId,", + " description: pm.variables.replaceIn('{{$randomLoremParagraphs}}'),", + " eventDate: this.getFutureDateTime(),", + " location: {", + " lat: parseFloat(pm.variables.replaceIn('{{$randomLatitude}}')),", + " lon: parseFloat(pm.variables.replaceIn('{{$randomLongitude}}')),", + " },", + " paid: pm.variables.replaceIn('{{$randomBoolean}}'),", + " participantLimit: pm.variables.replaceIn('{{$randomInt}}'),", + " requestModeration: pm.variables.replaceIn('{{$randomBoolean}}'),", + " title: pm.variables.replaceIn('{{$randomLoremSentence}}'),", + " }", + " }", + " ", + " getCompilation(...eventIds) { ", + " return { ", + " title: pm.variables.replaceIn('{{$randomLoremSentence}}'), ", + " pinned: pm.variables.replaceIn('{{$randomBoolean}}'), ", + " events: eventIds ", + " }; ", + " }", + "", + " getPastDateTime(hourShift = 5, minuteShift=0, yearShift=0) {", + " let moment = require('moment');", + "", + " let m = moment();", + " m.subtract(hourShift, 'hour');", + " m.subtract(minuteShift, 'minute');", + " m.subtract(yearShift, 'year');", + "", + " return m.format('YYYY-MM-DD HH:mm:ss');", + " }", + "", + " getFutureDateTime(hourShift = 5, minuteShift=0, yearShift=0) {", + " let moment = require('moment');", + "", + " let m = moment();", + " m.add(hourShift, 'hour');", + " m.add(minuteShift, 'minute');", + " m.add(yearShift, 'year');", + "", + " return m.format('YYYY-MM-DD HH:mm:ss');", + " }", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "http://localhost:9090", + "type": "string" + }, + { + "key": "uri", + "value": "1" + }, + { + "key": "source", + "value": "" + } + ] +} \ No newline at end of file diff --git a/Postman/explore-with-me.postman_collection.json b/Postman/explore-with-me.postman_collection.json deleted file mode 100644 index 903046e..0000000 --- a/Postman/explore-with-me.postman_collection.json +++ /dev/null @@ -1,540 +0,0 @@ -{ - "info": { - "_postman_id": "abbfaa88-2ad4-447b-a989-bb1f6dab9637", - "name": "explore-with-me", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "39468895" - }, - "item": [ - { - "name": "Stat", - "item": [ - { - "name": "Add Hit", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"app\": \"ewm-main-service\",\r\n \"uri\": \"/events/2\",\r\n \"ip\": \"192.163.0.1\",\r\n \"timestamp\": \"2022-09-06 11:00:23\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://localhost:9090/hit", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "9090", - "path": [ - "hit" - ] - } - }, - "response": [] - }, - { - "name": "get Stat", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"app\": \"ewm-main-service\",\r\n \"uri\": \"/events/1\",\r\n \"ip\": \"192.163.0.1\",\r\n \"timestamp\": \"2022-09-06 11:00:23\"\r\n}" - }, - "url": { - "raw": "http://localhost:9090/stats?uris=/events/1&uris=/events/2&unique=false", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "9090", - "path": [ - "stats" - ], - "query": [ - { - "key": "uris", - "value": "/events/1" - }, - { - "key": "uris", - "value": "/events/2" - }, - { - "key": "unique", - "value": "false" - } - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Admin", - "item": [ - { - "name": "users", - "item": [ - { - "name": "Get users", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"app\": \"ewm-main-service\",\r\n \"uri\": \"/events/2\",\r\n \"ip\": \"192.163.0.1\",\r\n \"timestamp\": \"2022-09-06 11:00:23\"\r\n}" - }, - "url": { - "raw": "http://localhost:8080/admin/users", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8080", - "path": [ - "admin", - "users" - ] - } - }, - "response": [] - }, - { - "name": "Create user", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"email\": \"ivan.petrov@practicummail.ru\",\r\n \"name\": \"Иван Петров\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://localhost:8080/admin/users", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8080", - "path": [ - "admin", - "users" - ] - } - }, - "response": [] - }, - { - "name": "Delete user", - "request": { - "method": "DELETE", - "header": [], - "url": { - "raw": "http://localhost:8080/admin/users/2", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8080", - "path": [ - "admin", - "users", - "2" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Categories", - "item": [ - { - "name": "Create category", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"email\": \"ivan.petrov@practicummail.ru\",\r\n \"name\": \"Иван Петров\"\r\n}" - }, - "url": { - "raw": "http://localhost:8080/admin/catgories", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8080", - "path": [ - "admin", - "catgories" - ] - } - }, - "response": [] - }, - { - "name": "Patch category", - "request": { - "method": "PATCH", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"name\": \"Соревнования\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://localhost:8080/admin/categories/2", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8080", - "path": [ - "admin", - "categories", - "2" - ] - } - }, - "response": [] - }, - { - "name": "Delete category", - "request": { - "method": "DELETE", - "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://localhost:8080/admin/categories/3", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8080", - "path": [ - "admin", - "categories", - "3" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Events", - "item": [ - { - "name": "Patch event", - "request": { - "method": "PATCH", - "header": [], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "http://localhost:8080/admin/events/7", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8080", - "path": [ - "admin", - "events", - "7" - ] - } - }, - "response": [] - } - ] - } - ] - }, - { - "name": "Users", - "item": [ - { - "name": "Events", - "item": [ - { - "name": "Create event", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"annotation\": \"Сплав на байдарках похож на полет.\",\r\n \"category\": 7,\r\n \"description\": \"Сплав на байдарках похож на полет. На спокойной воде — это парение. На бурной, порожистой — выполнение фигур высшего пилотажа. И то, и другое дарят чувство обновления, феерические эмоции, яркие впечатления.\",\r\n \"eventDate\": \"2025-05-31 23:55:05\",\r\n \"location\": {\r\n \"lat\": 55.754167,\r\n \"lon\": 37.62\r\n },\r\n \"paid\": true,\r\n \"participantLimit\": 10,\r\n \"requestModeration\": false,\r\n \"title\": \"Сплав на байдарках\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://localhost:8080/users/1/events", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8080", - "path": [ - "users", - "1", - "events" - ] - } - }, - "response": [] - }, - { - "name": "Get event", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "http://localhost:8080/users/2/events/7", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8080", - "path": [ - "users", - "2", - "events", - "7" - ] - } - }, - "response": [] - }, - { - "name": "Get events", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "http://localhost:8080/users/1/events", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8080", - "path": [ - "users", - "1", - "events" - ] - } - }, - "response": [] - }, - { - "name": "Update event", - "request": { - "method": "PATCH", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"eventDate\": \"2025-09-31 18:00:00\",\r\n \"title\": \"Концерт рок-группы 'Java Core'\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://localhost:8080/users/1/events/2", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8080", - "path": [ - "users", - "1", - "events", - "2" - ] - } - }, - "response": [] - }, - { - "name": "Get request by event", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "http://localhost:8080/users/3/events2/reqests", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8080", - "path": [ - "users", - "3", - "events2", - "reqests" - ] - } - }, - "response": [] - }, - { - "name": "Patch requests", - "request": { - "method": "PATCH", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"requestIds\": [\r\n 1,\r\n 2,\r\n 3\r\n ],\r\n \"status\": \"CONFIRMED\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://localhost:8080/users/1/events/2/requests", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8080", - "path": [ - "users", - "1", - "events", - "2", - "requests" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Requests", - "item": [ - { - "name": "Create request", - "request": { - "method": "POST", - "header": [], - "url": { - "raw": "http://localhost:8080/users/1/requests?eventId=8", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8080", - "path": [ - "users", - "1", - "requests" - ], - "query": [ - { - "key": "eventId", - "value": "8" - } - ] - } - }, - "response": [] - }, - { - "name": "Get requests", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "http://localhost:8080/users/2/requests", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8080", - "path": [ - "users", - "2", - "requests" - ] - } - }, - "response": [] - }, - { - "name": "Delete request", - "request": { - "method": "DELETE", - "header": [], - "url": { - "raw": "http://localhost:8080/users/3/requests/4/cancel", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8080", - "path": [ - "users", - "3", - "requests", - "4", - "cancel" - ] - } - }, - "response": [] - } - ] - } - ] - } - ] -} \ No newline at end of file diff --git a/Postman/objects.json b/Postman/objects.json deleted file mode 100644 index 855d29b..0000000 --- a/Postman/objects.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "users": [ - { - "id": 1, - "name": "Иван Петров", - "email": "ivan.petrov@practicum.mail.ru" - }, - { - "id": 2, - "name": "Николай Кузнецов", - "email": "nikolay.ruznetsov@practicum.mail.ru" - }, - { - "id": 3, - "name": "Владимир Петров", - "email": "vladimir.petrov@practicum.mail.ru" - } - ], - -"events": [ - { - "id": 1, - "annotation": "Сплав на байдарках похож на полет.", - "category": { - "id": 2, - "name": "Соревнования" - }, - "confirmedRequest": null, - "eventDate": "2025-06-30T23:55:05", - "initiator": { - "id": 1, - "name": "Иван Петров" - }, - "paid": true, - "title": "Сплав на байдарках", - "views": null - }, - { - "id": 2, - "annotation": "За почти три десятилетия группа 'Java Core' закрепились на сцене как группа, объединяющая поколения.", - "category": { - "id": 1, - "name": "Концерты" - }, - "confirmedRequest": null, - "eventDate": "2025-09-30T18:00:00", - "initiator": { - "id": 1, - "name": "Иван Петров" - }, - "paid": true, - "title": "Концерт рок-группы 'Java Core'", - "views": null - } -] -} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 4af02be..8ccf9e7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,6 +40,9 @@ services: - stats-server environment: - STATSERVER_URL=http://stats-server:9090 + - SPRING_DATASOURCE_URL=jdbc:postgresql://ewm-db:5432/ewmdb + - SPRING_DATASOURCE_USERNAME=ewmdb + - SPRING_DATASOURCE_PASSWORD=ewmdb ewm-db: image: postgres:16.1 diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java b/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java index f40fc95..176eab7 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java @@ -12,7 +12,6 @@ import ru.practicum.statclient.BaseClient; import ru.practicum.statdto.HitDto; import ru.practicum.statdto.StatsDto; -import ru.practicum.statdto.StatsDtoList; import java.time.LocalDateTime; import java.util.ArrayList; @@ -20,11 +19,17 @@ import java.util.List; import java.util.Map; +// class StatsDtoListTypeToken extends TypeToken> { +// } + @Component public class StatsClient extends BaseClient { private static final String PREFIX_HIT = "/hit"; private static final String PREFIX_STATS = "/stats"; private static final String PREFIX_EVETS = "/events/"; + // private static Gson gson = new GsonBuilder() + // .setPrettyPrinting() + // .create(); @Autowired public StatsClient(@Value("${statserver.url}") String serverUrl, RestTemplateBuilder builder) { @@ -53,6 +58,7 @@ public void hitInfo(String appName, HttpServletRequest request) { post(hitDto); } + public Integer getEventViews(Integer eventId, Boolean unique) { Map parameters = Map.of("uris", PREFIX_EVETS + eventId, "unique", unique); diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java index 955e6d7..0ebc957 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java @@ -15,6 +15,7 @@ import ru.practicum.evmsevice.model.Event; import ru.practicum.evmsevice.model.User; import ru.practicum.evmsevice.service.CategoryService; +import ru.practicum.evmsevice.service.CompilationService; import ru.practicum.evmsevice.service.EventService; import ru.practicum.evmsevice.service.UserService; @@ -34,14 +35,27 @@ public class AdminController { private final UserService userService; private final CategoryService categoryService; private final EventService eventService; + private final CompilationService compilationService; @GetMapping("/users") @ResponseStatus(HttpStatus.OK) - public List getUsers(HttpServletRequest request) { - log.info("{} запрашивает список пользователей.", request.getRemoteUser()); + public List getUsers( + @RequestParam(name = "ids", required = false) List ids, + @RequestParam(name = "from", defaultValue = "0") Integer from, + @RequestParam(name = "size", defaultValue = "10") Integer size, + HttpServletRequest request) { + log.info("Администратор запрашивает запрашивает список пользователей. {}", ids); statsClient.hitInfo(appName, request); - return userService.getUsers().stream() + List users; + if (ids != null) { + users = userService.getUsers(ids); + } else { + users = userService.getUsers(); + } + return users.stream() .map(UserMapper::toUserDto) + .skip(from) + .limit(size) .toList(); } @@ -64,7 +78,7 @@ public UserDto createUser(@Validated @RequestBody UserDto userDto, HttpServletRe } @DeleteMapping("/users/{id}") - @ResponseStatus(HttpStatus.OK) + @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteUser(HttpServletRequest request, @PathVariable Integer id) { log.info("Удаляем пользователя {}", id); statsClient.hitInfo(appName, request); @@ -121,8 +135,30 @@ public List findEvents( @PatchMapping("/events/{eventId}") @ResponseStatus(HttpStatus.OK) public EventFullDto updateEvent(@PathVariable Integer eventId, - @RequestBody UpdateEventAdminRequest eventDto) { - log.info("Администратор модерирует событие id={}.", eventId); + @Validated @RequestBody UpdateEventAdminRequest eventDto) { + log.info("Администратор редактирует событие id={}. {}", eventId, eventDto); return eventService.adminUpdateEvent(eventId, eventDto); } + + @PostMapping("/compilations") + @ResponseStatus(HttpStatus.CREATED) + public CompilationDto createCompilation(@Validated @RequestBody NewCompilationDto newCompilationDto) { + log.info("Администратор создает подборку событий \'{}\'.", newCompilationDto.getTitle()); + return compilationService.createCompilation(newCompilationDto); + } + + @PatchMapping("/compilations/{compId}") + @ResponseStatus(HttpStatus.OK) + public CompilationDto updateCompilation(@PathVariable Integer compId, + @Validated @RequestBody PatchCompilationDto compilationDto) { + log.info("Администратор обновляет подборку событий \'{}\'.", compilationDto.getTitle()); + return compilationService.patchCompilation(compId, compilationDto); + } + + @DeleteMapping("/compilations/{compId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteCompilation(@PathVariable Integer compId) { + log.info("Администратор удаляет подборку событий id={}.", compId); + compilationService.deleteCompilation(compId); + } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java index 0fac822..61bd037 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java @@ -4,19 +4,22 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.web.ErrorResponse; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.HandlerMethodValidationException; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import ru.practicum.evmsevice.dto.ApiError; -import ru.practicum.evmsevice.exception.DataConflictException; -import ru.practicum.evmsevice.exception.InternalServerException; -import ru.practicum.evmsevice.exception.NotFoundException; -import ru.practicum.evmsevice.exception.ValidationException; +import ru.practicum.evmsevice.exception.*; import java.time.LocalDateTime; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; /** @@ -25,6 +28,17 @@ @Slf4j @RestControllerAdvice public class ErrorAdvisor { + @ExceptionHandler(BadRequestException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiError onHandlerMethodValidationException(BadRequestException e) { + log.error("400 {}.", e.getMessage()); + ApiError apiError = new ApiError(); + apiError.setStatus(HttpStatus.BAD_REQUEST); + apiError.setReason("Запрос составлен некорректно."); + apiError.setMessage(e.getMessage()); + apiError.setTimestamp(LocalDateTime.now()); + return apiError; + } @ExceptionHandler(NotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) @@ -74,7 +88,6 @@ public ApiError onDataIntegrityViolationException(final DataConflictException e) return apiError; } - @ExceptionHandler(DataIntegrityViolationException.class) @ResponseStatus(HttpStatus.CONFLICT) public ApiError onDataIntegrityViolationException(final DataIntegrityViolationException e) { @@ -141,7 +154,7 @@ public ApiError onMethodArgumentTypeMismatchException(MethodArgumentTypeMismatch @ExceptionHandler @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ApiError handleException(final Exception e) { - log.error("Error", e); + log.error("500 INTERNAL_SERVER_ERROR", e); ApiError apiError = new ApiError(); apiError.setStatus(HttpStatus.INTERNAL_SERVER_ERROR); apiError.setReason(e.getCause().getMessage()); diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java index 7cb1efe..248983b 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java @@ -1,40 +1,46 @@ package ru.practicum.evmsevice.controller; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Pattern; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import ru.practicum.evmsevice.client.StatsClient; +import ru.practicum.evmsevice.dto.CompilationDto; import ru.practicum.evmsevice.dto.EventFullDto; import ru.practicum.evmsevice.dto.EventShortDto; import ru.practicum.evmsevice.enums.EventState; import ru.practicum.evmsevice.exception.NotFoundException; import ru.practicum.evmsevice.mapper.EventMapper; +import ru.practicum.evmsevice.model.Category; import ru.practicum.evmsevice.model.Event; +import ru.practicum.evmsevice.service.CategoryService; +import ru.practicum.evmsevice.service.CompilationService; import ru.practicum.evmsevice.service.EventService; -import java.util.ArrayList; import java.util.List; -import static ru.practicum.evmsevice.mapper.EventMapper.toFullDto; - @Slf4j @RequiredArgsConstructor @RestController @RequestMapping() public class PublicController { + private static final String RGEXP_DATE_TIME = "yyyy-MM-dd' 'HH:mm:ss"; @Value("${spring.application.name}") private String appName; private final StatsClient statsClient; private final EventService eventService; + private final CompilationService compilationService; + private final CategoryService categoryService; @GetMapping("/events/{id}") @ResponseStatus(HttpStatus.OK) public EventFullDto findEventById(@PathVariable("id") int id, HttpServletRequest request) { - log.info("Пользователь запрвшмвает для просмотра событие: {}", id); + log.info("Пользователь запрашивает для просмотра событие: {}", id); Event event = eventService.findEventById(id); // Событие должно быть опубликовано if (!event.getState().equals(EventState.PUBLISHED)) { @@ -42,36 +48,64 @@ public EventFullDto findEventById(@PathVariable("id") int id, } // сохраняем запрос в сервере статистики statsClient.hitInfo(appName, request); - EventFullDto eventFullDto = EventMapper.toFullDto(event); - eventFullDto.setViews(statsClient.getEventViews(id, true)); - return eventFullDto; + return EventMapper.toFullDto(event); } @GetMapping("/events") @ResponseStatus(HttpStatus.OK) public List findAllEvents( - @RequestParam(name = "text", required = false) String text, - @RequestParam(name = "categories", required = false) List categories, - @RequestParam(name = "paid", required = false) Boolean paid, - @RequestParam(name = "rangeStart", required = false) String rangeStart, - @RequestParam(name = "rangeEnd", required = false) String rangeEnd, - @RequestParam(name = "onlyAvailable", defaultValue = "false") Boolean onlyAvailable, - @RequestParam(name = "sort", defaultValue = "EVENT_DATE") String sort, - @RequestParam(name = "from", defaultValue = "0") Integer from, - @RequestParam(name = "size", defaultValue = "10") Integer size, - HttpServletRequest request) { - log.info("Пользователь запрвшмвает поиск событий."); - List eventDtos = eventService.findEventsByParametrs( - text, - categories, - paid, - rangeStart, - rangeEnd, - onlyAvailable, - sort, - from, - size - ); + @RequestParam(name = "text", required = false) String text, + @RequestParam(name = "categories", required = false) List categories, + @RequestParam(name = "paid", required = false) Boolean paid, + @RequestParam(name = "rangeStart", required = false) + //@Valid @Pattern(regexp = RGEXP_DATE_TIME, message = "Не верный формат даты.") + String rangeStart, + @RequestParam(name = "rangeEnd", required = false) + //@Valid @Pattern(regexp = RGEXP_DATE_TIME, message = "Не верный формат даты.") + String rangeEnd, + @RequestParam(name = "onlyAvailable", defaultValue = "false") Boolean onlyAvailable, + @RequestParam(name = "sort", defaultValue = "EVENT_DATE") String sort, + @RequestParam(name = "from", defaultValue = "0") Integer from, + @RequestParam(name = "size", defaultValue = "10") Integer size, + HttpServletRequest request) { + log.info("Пользователь запрашивает поиск событий: содержащих текст:{}, categories:{}, rangeStart:{}, rangeEnd:{}.", + text, categories, rangeStart, rangeEnd); + List eventDtos = eventService.findEventsByParametrs(text, categories, + paid, rangeStart, rangeEnd, onlyAvailable, sort, from, size); + statsClient.hitInfo(appName, request); return eventDtos; } + + @GetMapping("/compilations") + @ResponseStatus(HttpStatus.OK) + public List findAllCompilations( + @RequestParam(name = "pinned", required = false) Boolean pinned, + @RequestParam(name = "from", defaultValue = "0") Integer from, + @RequestParam(name = "size", defaultValue = "10") Integer size) { + log.info("Пользователь запрашивает список подборок."); + return compilationService.getCompilations(pinned, from, size); + } + + @GetMapping("/compilations/{compId}") + @ResponseStatus(HttpStatus.OK) + public CompilationDto findCompilationById(@PathVariable("compId") int compId) { + log.info("Пользователь запрашивает подборку id={}.", compId); + return compilationService.getCompilation(compId); + } + + @GetMapping("/categories") + @ResponseStatus(HttpStatus.OK) + public List findCategories(@RequestParam(name = "from", defaultValue = "0") Integer from, + @RequestParam(name = "size", defaultValue = "10") Integer size) { + log.info("Пользователь запрашивает список категорий."); + return categoryService.getAllCategories().stream().skip(from).limit(size).toList(); + } + + @GetMapping("/categories/{catId}") + @ResponseStatus(HttpStatus.OK) + public Category findCategoryById(@PathVariable("catId") int catId) { + log.info("Пользователь запрашивает категорию id={}.", catId); + return categoryService.getCategoryById(catId); + } + } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/TestClientController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/TestClientController.java index c817743..e51b2a2 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/TestClientController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/TestClientController.java @@ -30,7 +30,7 @@ public void hit(@RequestBody HitDto dto) { @GetMapping("/stats") @ResponseStatus(HttpStatus.OK) - public ResponseEntity getStats( + public String getStats( @RequestParam(required = false) String start, @RequestParam(required = false) String end, @RequestParam(required = false) String uris, @@ -45,7 +45,7 @@ public ResponseEntity getStats( if (unique != null) parameters.put("unique", unique); if (size != null) parameters.put("size", size); ResponseEntity response = statsClient.get(parameters); - return response; + return response.getBody().toString(); } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java index 5599271..6bfefbf 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java @@ -25,7 +25,7 @@ public class UserController { @ResponseStatus(HttpStatus.CREATED) public EventFullDto createEvent(@PathVariable int id, @Validated @RequestBody NewEventDto eventDto) { - log.info("Пользователь id={} cоздает новое событие: {}", id, eventDto.getTitle()); + log.info("Пользователь id={} cоздает новое событие: {}", id, eventDto.toString()); return eventService.createEvent(eventDto, id); } @@ -52,9 +52,8 @@ public List getEvents(@PathVariable int userId, @ResponseStatus(HttpStatus.OK) public EventFullDto updateEvent(@PathVariable Integer userId, @PathVariable Integer eventId, - @RequestBody UpdateEventUserRequest eventDto) { - log.info("Пользователь id={} изменяет информацию об инциированном событии id={}.", - userId, eventId); + @Validated @RequestBody UpdateEventUserRequest eventDto) { + log.info("Пользователь id={} изменяет информацию об инциированном событии. {}" , userId, eventDto.toString()); return eventService.patchEvent(eventId, eventDto, userId); } @@ -80,7 +79,7 @@ public RequestGroupDto patchRequestsByEventId(@PathVariable int userId, return requestService.updateRequestsStatus(userId, eventId, requestUpdateDto); } - @PostMapping("/{userId}/requests") + @PostMapping("/{userId}/requests") @ResponseStatus(HttpStatus.CREATED) public RequestDto createRequest(@PathVariable Integer userId, @RequestParam(name = "eventId", required = true) Integer eventId) { @@ -93,18 +92,15 @@ public RequestDto createRequest(@PathVariable Integer userId, @GetMapping("/{userId}/requests") @ResponseStatus(HttpStatus.OK) public List findRequestsByUserId(@PathVariable Integer userId) { - log.info("Пользователь id={} выполняет поиск собственных запросов.", userId); - return requestService.getRequestsByUserId(userId) - .stream() - .map(RequestMapper::toRequestDto) - .toList(); + log.info("Пользователь id={} выполняет поиск собственных заявок.", userId); + return requestService.getRequestsByUserId(userId); } - @DeleteMapping("/{userId}/requests/{requestId}/cancel") + @PatchMapping("/{userId}/requests/{requestId}/cancel") @ResponseStatus(HttpStatus.OK) - public RequestDto deleteRequestById(@PathVariable Integer userId, + public RequestDto canceledRequestById(@PathVariable Integer userId, @PathVariable Integer requestId) { - log.info("Пользователь id={} удаляет запрос id={}.", userId, requestId); - return RequestMapper.toRequestDto(requestService.deleteRequest(userId, requestId)); + log.info("Пользователь id={} отменяет запрос id={}.", userId, requestId); + return RequestMapper.toRequestDto(requestService.CanceledRequest(userId, requestId)); } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CompilationDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CompilationDto.java new file mode 100644 index 0000000..4330031 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CompilationDto.java @@ -0,0 +1,21 @@ +package ru.practicum.evmsevice.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.evmsevice.model.Event; + +import java.util.ArrayList; +import java.util.List; + +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class CompilationDto { + private int id; + private List events = new ArrayList<>(); + private String title; + private Boolean pinned; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventShortDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventShortDto.java index 9d1b327..f8aa42c 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventShortDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/EventShortDto.java @@ -17,7 +17,7 @@ public class EventShortDto { private Integer id; private String annotation; private CategoryDto category; - private Integer confirmedRequest; + private Integer confirmedRequests; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime eventDate; private UserShortDto initiator; diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewCompilationDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewCompilationDto.java new file mode 100644 index 0000000..21e37ef --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewCompilationDto.java @@ -0,0 +1,25 @@ +package ru.practicum.evmsevice.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class NewCompilationDto { + private List events = new ArrayList<>(); + @NotEmpty(message = "Заголовок подборки не может быть пустым.") + @NotBlank(message = "Заголовок подборки не может быть пустым.") + @Size(min = 2, max = 50, message = "длина описания 2 - 50 символов.") + private String title; + private Boolean pinned; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewEventDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewEventDto.java index d50b20f..d24dcf2 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewEventDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewEventDto.java @@ -1,9 +1,7 @@ package ru.practicum.evmsevice.dto; import com.fasterxml.jackson.annotation.JsonFormat; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; +import jakarta.validation.constraints.*; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -18,19 +16,27 @@ @ToString public class NewEventDto { + @NotEmpty(message = "Аннотация не может быть пустой.") + @NotBlank(message = "Аннотация не может быть пустой.") @Size(min = 20, max = 2000, message = "длина аннотации 20 - 2000 символов.") private String annotation; @NotNull(message = "Категоря должна быть определена") private Integer category; + @NotEmpty(message = "Описание события не может быть пустым.") + @NotBlank(message = "Описание события не может быть пустым.") @Size(min = 20, max = 7000, message = "длина описания 20 - 7000 символов.") private String description; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Future(message = "дата события не должна быть уже наступившей.") private LocalDateTime eventDate; @NotNull(message = "Место события должно быть определено.") private Location location; private Boolean paid; + @PositiveOrZero private Integer participantLimit; private Boolean requestModeration; + @NotEmpty(message = "Заголовок не может быть пустым.") + @NotBlank(message = "Заголовок не может быть пустым.") @Size(min = 3, max = 120, message = "длина заголовка 3 - 120 символов.") private String title; } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/PatchCompilationDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/PatchCompilationDto.java new file mode 100644 index 0000000..0ca90cb --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/PatchCompilationDto.java @@ -0,0 +1,24 @@ +package ru.practicum.evmsevice.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class PatchCompilationDto { + private List events = new ArrayList<>(); + @Size(min = 2, max = 50, message = "длина описания 2 - 50 символов.") + private String title; + private Boolean pinned; + +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UpdateEventAdminRequest.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UpdateEventAdminRequest.java index a8d1244..c668309 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UpdateEventAdminRequest.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UpdateEventAdminRequest.java @@ -1,13 +1,37 @@ package ru.practicum.evmsevice.dto; +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.ToString; import ru.practicum.evmsevice.enums.EventAdminAction; +import ru.practicum.evmsevice.model.Location; + +import java.time.LocalDateTime; @Setter @Getter @NoArgsConstructor -public class UpdateEventAdminRequest extends NewEventDto{ +@ToString +public class UpdateEventAdminRequest { + @Size(min = 20, max = 2000, message = "длина аннотации 20 - 2000 символов.") + private String annotation; + private Integer category; + @Size(min = 20, max = 7000, message = "длина описания 20 - 7000 символов.") + private String description; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Future(message = "дата события не должна быть уже наступившей.") + private LocalDateTime eventDate; + private Location location; + private Boolean paid; + @Positive(message = "число участников должно быть положительным") + private Integer participantLimit; + private Boolean requestModeration; + @Size(min = 3, max = 120, message = "длина заголовка 3 - 120 символов.") + private String title; private EventAdminAction stateAction; } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UpdateEventUserRequest.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UpdateEventUserRequest.java index f841f22..76eb925 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UpdateEventUserRequest.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UpdateEventUserRequest.java @@ -1,14 +1,33 @@ package ru.practicum.evmsevice.dto; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.*; +import lombok.*; import ru.practicum.evmsevice.enums.EventState; import ru.practicum.evmsevice.enums.EventUserAction; +import ru.practicum.evmsevice.model.Location; + +import java.time.LocalDateTime; @Setter @Getter @NoArgsConstructor -public class UpdateEventUserRequest extends NewEventDto{ +@ToString +public class UpdateEventUserRequest{ + @Size(min = 20, max = 2000, message = "длина аннотации 20 - 2000 символов.") + private String annotation; + private Integer category; + @Size(min = 20, max = 7000, message = "длина описания 20 - 7000 символов.") + private String description; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Future(message = "дата события не должна быть уже наступившей.") + private LocalDateTime eventDate; + private Location location; + private Boolean paid; + @Positive(message = "число участников должно быть положительным") + private Integer participantLimit; + private Boolean requestModeration; + @Size(min = 3, max = 120, message = "длина заголовка 3 - 120 символов.") + private String title; private EventUserAction stateAction; } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UserDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UserDto.java index 4f30d32..a47a36b 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UserDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UserDto.java @@ -2,6 +2,7 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; import lombok.*; @Setter @@ -12,8 +13,10 @@ public class UserDto { private Integer id; @NotBlank(message = "Имя не может быть пустым") + @Size(min = 2, max = 250, message = "длина имени должна быть 2 - 250 символов.") private String name; @NotBlank(message = "Email не может быть пустым") @Email(message = "Email должен удовлетворять правилам формирования почтовых адресов.") + @Size(min = 6, max = 254, message = "длина имени должна быть 6 - 254 символов.") private String email; } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/exception/BadRequestException.java b/ewm-service/src/main/java/ru/practicum/evmsevice/exception/BadRequestException.java new file mode 100644 index 0000000..0072153 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/exception/BadRequestException.java @@ -0,0 +1,7 @@ +package ru.practicum.evmsevice.exception; + +public class BadRequestException extends RuntimeException { + public BadRequestException(String message) { + super(message); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CompilationMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CompilationMapper.java new file mode 100644 index 0000000..460e32f --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CompilationMapper.java @@ -0,0 +1,36 @@ +package ru.practicum.evmsevice.mapper; + +import ru.practicum.evmsevice.dto.CompilationDto; +import ru.practicum.evmsevice.dto.EventShortDto; +import ru.practicum.evmsevice.dto.NewCompilationDto; +import ru.practicum.evmsevice.model.Compilation; + +import java.util.List; + +public class CompilationMapper { + private CompilationMapper() {} + + public static Compilation toCompilation(NewCompilationDto dto) { + Compilation c = new Compilation(); + c.setTitle(dto.getTitle()); + c.setPinned(false); + if (dto.getPinned() != null) { + c.setPinned(dto.getPinned()); + } + return c; + } + + public static CompilationDto toCompilationDto(Compilation c) { + CompilationDto dto = new CompilationDto(); + dto.setId(c.getId()); + dto.setTitle(c.getTitle()); + dto.setPinned(c.getPinned()); + List eventDtos = c.getEvents() + .stream() + .map(EventMapper::toShortDto) + .toList(); + dto.setEvents(eventDtos); + return dto; + } + +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java index daae687..fa59191 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java @@ -28,12 +28,14 @@ public static Event toEvent(final NewEventDto newDto) { if(newDto.getParticipantLimit() != null) { event.setParticipantLimit(newDto.getParticipantLimit()); } - event.setRequestModeration(false); + event.setRequestModeration(true); if (newDto.getRequestModeration() != null) { event.setRequestModeration(newDto.getRequestModeration()); } event.setState(EventState.PENDING); event.setTitle(newDto.getTitle()); + event.setConfirmedRequests(0); + event.setViews(0); return event; } @@ -57,7 +59,13 @@ public static EventFullDto toFullDto(Event event) { dto.setPublishedOn(event.getPublishedOn()); dto.setTitle(event.getTitle()); dto.setConfirmedRequests(0); + if (event.getConfirmedRequests() != null) { + dto.setConfirmedRequests(event.getConfirmedRequests()); + } dto.setViews(0); + if(event.getViews() != null) { + dto.setViews(event.getViews()); + } return dto; } @@ -74,8 +82,14 @@ public static EventShortDto toShortDto(Event event) { if(event.getParticipantLimit() != null) { dto.setParticipantLimit(event.getParticipantLimit()); } - dto.setConfirmedRequest(0); + dto.setConfirmedRequests(0); + if (event.getConfirmedRequests() != null) { + dto.setConfirmedRequests(event.getConfirmedRequests()); + } dto.setViews(0); + if(event.getViews() != null) { + dto.setViews(event.getViews()); + } return dto; } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Compilation.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Compilation.java new file mode 100644 index 0000000..8d7d04f --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Compilation.java @@ -0,0 +1,32 @@ +package ru.practicum.evmsevice.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Entity +@Setter +@Getter +@Table(name = "compilations", schema = "public") +@NoArgsConstructor +public class Compilation { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.REFRESH) + @JoinTable(name = "eventlinks", + joinColumns = { @JoinColumn(name = "compilation_id") }, + inverseJoinColumns = { @JoinColumn(name = "event_id") }) + private Set events = new HashSet<>(); + //private List events = new ArrayList<>(); + @Column(name = "title", nullable = false) + private String title; + @Column(name = "pinned", nullable = false) + private Boolean pinned; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java index 138dabb..22a67b6 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java @@ -1,5 +1,6 @@ package ru.practicum.evmsevice.model; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; @@ -7,6 +8,8 @@ import ru.practicum.evmsevice.enums.EventState; import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; @Entity @Setter @@ -48,4 +51,8 @@ public class Event { private EventState state; @Column(name = "title") private String title; + @Transient + private Integer confirmedRequests; + @Transient + private Integer views; } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CompilationRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CompilationRepository.java new file mode 100644 index 0000000..8450b70 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CompilationRepository.java @@ -0,0 +1,10 @@ +package ru.practicum.evmsevice.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.evmsevice.model.Compilation; + +import java.util.List; + +public interface CompilationRepository extends JpaRepository { + List findAllByPinnedEquals(boolean pinned); +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java index 380e3fa..9b66dca 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java @@ -10,4 +10,6 @@ public interface EventRepository extends JpaRepository, JpaSpecificationExecutor { List findEventsByInitiator_Id(int id); + + List findEventsByIdIn(List ids); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/UserRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/UserRepository.java index 5887768..4f06cc2 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/UserRepository.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/UserRepository.java @@ -3,5 +3,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import ru.practicum.evmsevice.model.User; +import java.util.List; + public interface UserRepository extends JpaRepository { } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryService.java index ca52503..9cc6049 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryService.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryService.java @@ -2,9 +2,16 @@ import ru.practicum.evmsevice.model.Category; +import java.util.List; + public interface CategoryService { Category createCategory(Category category); + Category updateCategory(Category category); + void deleteCategory(Integer id); + Category getCategoryById(Integer id); + + List getAllCategories(); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryServiceImpl.java index 5befe38..7ed6c84 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryServiceImpl.java @@ -7,6 +7,8 @@ import ru.practicum.evmsevice.model.Category; import ru.practicum.evmsevice.repository.CategoryRepository; +import java.util.List; + @Service @Transactional @RequiredArgsConstructor @@ -40,4 +42,9 @@ public Category getCategoryById(Integer id) { .orElseThrow(() -> new NotFoundException("Не найдена категория id=" + id)); return category; } + + @Override + public List getAllCategories() { + return categoryRepository.findAll(); + } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CompilationService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CompilationService.java new file mode 100644 index 0000000..48b1d5c --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CompilationService.java @@ -0,0 +1,19 @@ +package ru.practicum.evmsevice.service; + +import ru.practicum.evmsevice.dto.CompilationDto; +import ru.practicum.evmsevice.dto.NewCompilationDto; +import ru.practicum.evmsevice.dto.PatchCompilationDto; + +import java.util.List; + +public interface CompilationService { + CompilationDto createCompilation(NewCompilationDto compilationDto); + + CompilationDto patchCompilation(Integer compId, PatchCompilationDto compilationDto); + + void deleteCompilation(Integer compId); + + List getCompilations(Boolean pinned, Integer from, Integer size); + + CompilationDto getCompilation(Integer compId); +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CompilationServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CompilationServiceImpl.java new file mode 100644 index 0000000..b6ff4eb --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CompilationServiceImpl.java @@ -0,0 +1,85 @@ +package ru.practicum.evmsevice.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.evmsevice.dto.CompilationDto; +import ru.practicum.evmsevice.dto.NewCompilationDto; +import ru.practicum.evmsevice.dto.PatchCompilationDto; +import ru.practicum.evmsevice.exception.NotFoundException; +import ru.practicum.evmsevice.mapper.CompilationMapper; +import ru.practicum.evmsevice.model.Compilation; +import ru.practicum.evmsevice.model.Event; +import ru.practicum.evmsevice.repository.CompilationRepository; + +import java.util.HashSet; +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class CompilationServiceImpl implements CompilationService { + private final CompilationRepository compilationRepository; + private final EventService eventService; + + @Override + public CompilationDto createCompilation(NewCompilationDto compilationDto) { + Compilation compilation = CompilationMapper.toCompilation(compilationDto); + List events = eventService.findEventsByIdIn(compilationDto.getEvents()); + compilation.setEvents(new HashSet<>(events)); + Compilation savedCompilation = compilationRepository.save(compilation); + CompilationDto savedCompilationDto = CompilationMapper.toCompilationDto(savedCompilation); + return savedCompilationDto; + } + + @Override + public CompilationDto patchCompilation(Integer compId, PatchCompilationDto compilationDto) { + Compilation compilation = compilationRepository.findById(compId) + .orElseThrow(() -> new NotFoundException("Не найдена подборка id=" + compId)); + if (compilationDto.getTitle() != null) { + compilation.setTitle(compilationDto.getTitle()); + } + if (compilationDto.getPinned() != null) { + compilation.setPinned(compilationDto.getPinned()); + } + if (compilationDto.getEvents() != null) { + if (!compilationDto.getEvents().isEmpty()) { + List events = eventService.findEventsByIdIn(compilationDto.getEvents()); + compilation.setEvents(new HashSet<>(events)); + } + } + Compilation savedCompilation = compilationRepository.save(compilation); + CompilationDto savedCompilationDto = CompilationMapper.toCompilationDto(savedCompilation); + return savedCompilationDto; + } + + @Override + public void deleteCompilation(Integer compId) { + Compilation compilation = compilationRepository.findById(compId) + .orElseThrow(() -> new NotFoundException("Не найдена подборка id=" + compId)); + compilation.getEvents().clear(); + compilationRepository.delete(compilation); + } + + @Override + public CompilationDto getCompilation(Integer compId) { + Compilation compilation = compilationRepository.findById(compId) + .orElseThrow(() -> new NotFoundException("Не найдена подборка id=" + compId)); + return CompilationMapper.toCompilationDto(compilation); + } + + @Override + public List getCompilations(Boolean pinned, Integer from, Integer size) { + List compilations; + if (pinned != null) { + compilations = compilationRepository.findAllByPinnedEquals(pinned); + } else { + compilations = compilationRepository.findAll(); + } + return compilations.stream() + .map(CompilationMapper::toCompilationDto) + .skip(from) + .limit(size) + .toList(); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java index d1ee1be..cab08ba 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java @@ -35,4 +35,6 @@ List findEventsByAdmin(List states, String rangeEnd, Integer from, Integer size); + + List findEventsByIdIn(List eventIds); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java index 7d31c87..4d46ad2 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java @@ -3,14 +3,16 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; -import org.springframework.format.datetime.DateFormatter; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; import ru.practicum.evmsevice.client.StatsClient; import ru.practicum.evmsevice.dto.*; import ru.practicum.evmsevice.enums.EventAdminAction; import ru.practicum.evmsevice.enums.EventState; import ru.practicum.evmsevice.enums.EventUserAction; +import ru.practicum.evmsevice.exception.BadRequestException; +import ru.practicum.evmsevice.exception.DataConflictException; import ru.practicum.evmsevice.exception.NotFoundException; import ru.practicum.evmsevice.exception.ValidationException; import ru.practicum.evmsevice.mapper.EventMapper; @@ -23,19 +25,19 @@ import ru.practicum.evmsevice.repository.RequestRepository; import ru.practicum.statdto.StatsDto; -import java.time.Instant; -import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; -import java.util.*; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.TreeMap; @Service @Transactional @RequiredArgsConstructor -public class EventServiceImpl implements EventService{ +public class EventServiceImpl implements EventService { private static final DateTimeFormatter DATA_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - private static final DateTimeFormatter DATA_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final Integer HOURS_EVENT_DELAY = 2; private final EventRepository eventRepository; @@ -46,8 +48,9 @@ public class EventServiceImpl implements EventService{ /** * Создание нового события + * * @param newEventDto - новое событие - * @param userId - иденитификатор пользователя инициатора + * @param userId - иденитификатор пользователя инициатора * @return - сохраненный объект информации о событии */ @Override @@ -68,6 +71,13 @@ public EventFullDto createEvent(NewEventDto newEventDto, Integer userId) { return EventMapper.toFullDto(savedEvent); } + /** + * Получение подробной информации о событии инициатором + * + * @param eventId - идентификатор события + * @param userId - идентификатор инициатора + * @return - объект события + */ @Override public EventFullDto getEventById(Integer eventId, Integer userId) { Event event = findEventById(eventId); @@ -75,28 +85,72 @@ public EventFullDto getEventById(Integer eventId, Integer userId) { throw new ValidationException("Пользователь id=" + userId + " не является инициатором события id=" + eventId); } - EventFullDto eventFullDto = EventMapper.toFullDto(event); - eventFullDto.setConfirmedRequests(requestRepository.getCountConfirmedRequestsByEventId(eventId)); - eventFullDto.setViews(statsClient.getEventViews(eventId, true)); - return eventFullDto; + event.setConfirmedRequests(requestRepository.getCountConfirmedRequestsByEventId(eventId)); + event.setViews(statsClient.getEventViews(eventId, true)); + return EventMapper.toFullDto(event); } + /** + * Поиск событий по идентификатору инициатора + * + * @param userId - идентификатор инициатора + * @param from - с какого события отображать список + * @param size - размер списка + * @return - список событий + */ @Override public List getEventsByUserId(Integer userId, Integer from, Integer size) { List events = eventRepository.findEventsByInitiator_Id(userId); + updateViwesAndRequests(events); return events.stream() .skip(from) .limit(size) - .map(EventMapper::toShortDto).toList(); + .map(EventMapper::toShortDto) + .toList(); + } + + /** + * Заполняем объекты списка событий сведениями о просмотрах и подтвержденных заявках + * + * @param events - список событий + */ + private void updateViwesAndRequests(List events) { + TreeMap eventMap = new TreeMap(); + List eventUris = new ArrayList<>(); + for (Event event : events) { + eventMap.put(event.getId(), event); + eventUris.add(String.format("/events/%d", event.getId())); + } + // заполняем количество заявок + List counts = + requestRepository.getCountConfirmedRequests(eventMap.keySet().stream().toList()); + for (EventConfirmedRequestCount count : counts) { + Integer eventId = count.getEventId(); + eventMap.get(eventId).setConfirmedRequests(count.getConfirmedRequestCount().intValue()); + } + // заполняем количество просмотров + List statsDtos = statsClient.getEventViewsByUris(eventUris, true); + for (StatsDto dto : statsDtos) { + Integer eventId = Integer.parseInt(dto.getUri().split("/")[2]); + eventMap.get(eventId).setViews(dto.getHits()); + } } + /** + * Обновление события инициатором + * + * @param eventId - идентификатор события + * @param eventDto - объект с обновляемыми данными + * @param userId - идентификатор инициатора + * @return - обновленный объект события + */ @Override - public EventFullDto patchEvent(Integer eventId, UpdateEventUserRequest eventDto, Integer userId) { + public EventFullDto patchEvent(Integer eventId, @Validated UpdateEventUserRequest eventDto, Integer userId) { Event event = eventRepository.findById(eventId) .orElseThrow(() -> new NotFoundException("Не найдено событие id=" + eventId)); if (!event.getInitiator().getId().equals(userId)) { - throw new ValidationException("Пользователь id=" + userId + throw new DataConflictException("Пользователь id=" + userId + " не является инициатором события id=" + eventId); } if (event.getEventDate().isBefore(LocalDateTime.now().plusHours(HOURS_EVENT_DELAY))) { @@ -106,6 +160,11 @@ public EventFullDto patchEvent(Integer eventId, UpdateEventUserRequest eventDto, + event.getEventDate().format(DATA_TIME_FORMATTER) ); } + if (event.getState().equals(EventState.PUBLISHED)) { + throw new DataConflictException( + "Field: event.state. Error: Недопустимый статус события для изменения." + + " Value: " + event.getState()); + } if (eventDto.getAnnotation() != null) { event.setAnnotation(eventDto.getAnnotation()); } @@ -149,9 +208,18 @@ public EventFullDto patchEvent(Integer eventId, UpdateEventUserRequest eventDto, event.setTitle(eventDto.getTitle()); } Event savedEvent = eventRepository.save(event); + savedEvent.setConfirmedRequests(requestRepository.getCountConfirmedRequestsByEventId(eventId)); + savedEvent.setViews(statsClient.getEventViews(eventId, true)); return EventMapper.toFullDto(savedEvent); } + /** + * Обновление события администратором + * + * @param eventId - идентификатор события + * @param eventDto - объект с обновляемыми данными + * @return - обновленный объект события + */ @Override public EventFullDto adminUpdateEvent(Integer eventId, UpdateEventAdminRequest eventDto) { Event event = eventRepository.findById(eventId) @@ -199,7 +267,7 @@ public EventFullDto adminUpdateEvent(Integer eventId, UpdateEventAdminRequest ev if (eventDto.getStateAction() != null) { if (eventDto.getStateAction().equals(EventAdminAction.PUBLISH_EVENT)) { if (!event.getState().equals(EventState.PENDING)) { - throw new ValidationException( + throw new DataConflictException( "Field: stateAction. Error: " + "Событие id=" + eventId + " должно быть в состоянии ожидания публикации." + " Value: " + eventDto.getStateAction() @@ -208,20 +276,27 @@ public EventFullDto adminUpdateEvent(Integer eventId, UpdateEventAdminRequest ev event.setState(EventState.PUBLISHED); event.setPublishedOn(LocalDateTime.now()); } else if (eventDto.getStateAction().equals(EventAdminAction.REJECT_EVENT)) { - if(event.getState().equals(EventState.PUBLISHED)) { - throw new ValidationException( - "Field: stateAction. Error: " + - "Нельзя удалить опубликованное событие id=" + eventId + - " Value: " + eventDto.getStateAction() - ); + if (event.getState().equals(EventState.PUBLISHED)) { + throw new DataConflictException( + "Field: stateAction. Error: " + + "Нельзя удалить опубликованное событие id=" + eventId + + " Value: " + eventDto.getStateAction() + ); } event.setState(EventState.REJECTED); + } else { + throw new ValidationException( + "Field: stateAction. Error: " + + "Указано непредусмотренное действие. " + + " Value: " + eventDto.getStateAction()); } } if (eventDto.getTitle() != null) { event.setTitle(eventDto.getTitle()); } Event savedEvent = eventRepository.save(event); + savedEvent.setConfirmedRequests(requestRepository.getCountConfirmedRequestsByEventId(eventId)); + savedEvent.setViews(statsClient.getEventViews(eventId, true)); return EventMapper.toFullDto(savedEvent); } @@ -230,6 +305,8 @@ public Event findEventById(Integer eventId) { Event event = eventRepository.findById(eventId) .orElseThrow(() -> new NotFoundException("Не найдено событие id=" + eventId)); + event.setConfirmedRequests(requestRepository.getCountConfirmedRequestsByEventId(eventId)); + event.setViews(statsClient.getEventViews(eventId, true)); return event; } @@ -237,33 +314,40 @@ public Event findEventById(Integer eventId) { * поиск событий пользователем */ @Override - public List findEventsByParametrs(String text, - List categories, - Boolean paid, - String rangeStart, - String rangeEnd, - Boolean onlyAvailable, - String sort, - Integer from, - Integer size) { - - + public List findEventsByParametrs(String text, List categories, + Boolean paid, String rangeStart, String rangeEnd, + Boolean onlyAvailable, String sort, Integer from, Integer size) { LocalDateTime startDate = null; LocalDateTime endDate = null; - try { - if (rangeStart != null && !rangeStart.isEmpty()) { + + if (rangeStart != null && !rangeStart.isEmpty()) { + try { startDate = LocalDateTime.parse(rangeStart, DATA_TIME_FORMATTER); + } catch (DateTimeParseException e) { + throw new ValidationException("Некорректный формат времени. " + e.getMessage()); } - if (rangeEnd != null && !rangeEnd.isEmpty()) { + } + if (rangeEnd != null && !rangeEnd.isEmpty()) { + try { endDate = LocalDateTime.parse(rangeEnd, DATA_TIME_FORMATTER); + } catch (DateTimeParseException e) { + throw new ValidationException("Некорректный формат времени. " + e.getMessage()); } - // если в запросе не указан диапазон дат [rangeStart-rangeEnd], - // то нужно выгружать события, которые произойдут позже текущей даты и времени - if (startDate != null && endDate != null) { - startDate = LocalDateTime.now(); + } + if (startDate != null && endDate != null) { + if (startDate.isAfter(endDate)) { + throw new BadRequestException( + "Parametr: rangeStart, rangeEnd. " + + "Error: Введен некорректный интервал времени." + + ". Value: " + startDate.format(DATA_TIME_FORMATTER) + + ", " + endDate.format(DATA_TIME_FORMATTER) + ); } - } catch (DateTimeParseException e) { - throw new ValidationException("Некорректный формат времени. " + e.getMessage()); + } + // если в запросе не указан диапазон дат [rangeStart-rangeEnd], + // то нужно выгружать события, которые произойдут позже текущей даты и времени + if (startDate == null && endDate == null) { + startDate = LocalDateTime.now(); } Specification spec = Specification.where(null); @@ -290,48 +374,29 @@ public List findEventsByParametrs(String text, } List events = eventRepository.findAll(spec, - Sort.by("eventDate").descending()); - - TreeMap eventMap = new TreeMap(); - List eventUris = new ArrayList<>(); - for (Event event : events) { - eventMap.put(event.getId(), EventMapper.toShortDto(event)); - eventUris.add(String.format("/events/%d", event.getId())); - } - - // заполняем количество заявок - List counts = - requestRepository.getCountConfirmedRequests(eventMap.keySet().stream().toList()); - for(EventConfirmedRequestCount count : counts) { - Integer eventId = count.getEventId(); - eventMap.get(eventId).setConfirmedRequest(count.getConfirmedRequestCount().intValue()); - } - - // заполняем количество просмотров - List statsDtos = statsClient.getEventViewsByUris(eventUris, true); - for (StatsDto dto : statsDtos) { - Integer eventId = Integer.parseInt(dto.getUri().split("/")[2]); - eventMap.get(eventId).setViews(dto.getHits()); + Sort.by("eventDate")); + if (events.isEmpty()) { + return List.of(); } - - List eventDtos = new ArrayList<>(); - if(onlyAvailable) { - // Фильтруем события у котрыз не исчерпано количество заявок - eventDtos = eventMap.values() - .stream() - .filter(eventDto -> eventDto.getParticipantLimit() != 0 - && eventDto.getConfirmedRequest() < eventDto.getParticipantLimit()) + updateViwesAndRequests(events); + List eventDtos; + if (onlyAvailable) { + // Фильтруем события у котрых не исчерпано количество заявок + eventDtos = events.stream() + .filter(event -> event.getParticipantLimit() != 0 + && event.getConfirmedRequests() < event.getParticipantLimit()) + .map(EventMapper::toShortDto) .toList(); } else { - eventDtos.addAll(eventMap.values()); + eventDtos = events.stream().map(EventMapper::toShortDto).toList(); } - if(sort.equalsIgnoreCase("VIEWS")) { + if (sort.equalsIgnoreCase("VIEWS")) { return eventDtos.stream() - .sorted(Comparator.comparing(EventShortDto::getViews)) + .sorted(Comparator.comparing(EventShortDto::getViews).reversed()) .skip(from).limit(size).toList(); } - return eventMap.values().stream().skip(from).limit(size).toList(); + return eventDtos.stream().skip(from).limit(size).toList(); } /** @@ -372,7 +437,6 @@ public List findEventsByAdmin(List states, spec = spec.and(EventSpecification.categoryIn(categories)); } // ... поиск по списку состояний - // List enumStates = states.stream().map(state -> EventState.valueOf(state)).toList(); if (states != null) { spec = spec.and(EventSpecification.eventStateIn(states)); } @@ -385,35 +449,28 @@ public List findEventsByAdmin(List states, } List events = eventRepository.findAll(spec, - Sort.by("eventDate").descending()); + Sort.by("eventDate")); if (events.isEmpty()) { return List.of(); } + updateViwesAndRequests(events); + List eventDtos = events.stream() + .map(EventMapper::toFullDto) + .toList(); + return eventDtos.stream().skip(from).limit(size).toList(); + } - TreeMap eventMap = new TreeMap(); - List eventUris = new ArrayList<>(); - for (Event event : events) { - eventMap.put(event.getId(), EventMapper.toFullDto(event)); - eventUris.add(String.format("/events/%d", event.getId())); - } - - // заполняем количество заявок - List counts = - requestRepository.getCountConfirmedRequests(eventMap.keySet().stream().toList()); - for(EventConfirmedRequestCount count : counts) { - Integer eventId = count.getEventId(); - eventMap.get(eventId).setConfirmedRequests(count.getConfirmedRequestCount().intValue()); - } - - // заполняем количество просмотров - List statsDtos = statsClient.getEventViewsByUris(eventUris, true); - for (StatsDto dto : statsDtos) { - Integer eventId = Integer.parseInt(dto.getUri().split("/")[2]); - eventMap.get(eventId).setViews(dto.getHits()); + /** + * Поиск событий по списку идентификаторов + */ + @Override + public List findEventsByIdIn(List eventIds) { + List events = eventRepository.findEventsByIdIn(eventIds); + if (events.isEmpty()) { + return List.of(); } - - List eventDtos = new ArrayList<>(); - eventDtos.addAll(eventMap.values()); - return eventDtos.stream().skip(from).limit(size).toList(); + updateViwesAndRequests(events); + return events; } + } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java index 3ca5d19..d8a1ed1 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java @@ -1,5 +1,6 @@ package ru.practicum.evmsevice.service; +import ru.practicum.evmsevice.dto.RequestDto; import ru.practicum.evmsevice.dto.RequestGroupDto; import ru.practicum.evmsevice.dto.RequestUpdateDto; import ru.practicum.evmsevice.enums.RequestStatus; @@ -11,9 +12,9 @@ public interface RequestService { Request createRequest(Integer userId, Integer eventId ); - List getRequestsByUserId(Integer userId); + List getRequestsByUserId(Integer userId); - Request deleteRequest(Integer userId, Integer requestId); + Request CanceledRequest(Integer userId, Integer requestId); List getRequestsByEventId(Integer userId, Integer eventId); diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java index 728ad7b..de595f2 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import ru.practicum.evmsevice.dto.RequestDto; import ru.practicum.evmsevice.dto.RequestGroupDto; import ru.practicum.evmsevice.dto.RequestUpdateDto; import ru.practicum.evmsevice.enums.EventState; @@ -32,23 +33,23 @@ public class RequestServiceImpl implements RequestService { public Request createRequest(Integer userId, Integer eventId) { Event event = eventService.findEventById(eventId); if (event.getInitiator().getId().equals(userId)) { - throw new ValidationException( + throw new DataConflictException( "Field: event.initiator_id. Error: " + "Инициатор события не может добавить запрос на участие в своём событии. " + "Value: " + event.getInitiator().getId() ); } if (!event.getState().equals(EventState.PUBLISHED)) { - throw new ValidationException( + throw new DataConflictException( "Field: event.state. Error: " + "Нельзя участвовать в неопубликованном событии. " + "Value: " + event.getState() ); } - Integer confirmedRequests = requestRepository.getCountConfirmedRequestsByEventId(eventId); - if (confirmedRequests != null) { + Integer confirmedRequests = event.getConfirmedRequests(); + if (confirmedRequests != null && event.getParticipantLimit() > 0) { if (confirmedRequests.equals(event.getParticipantLimit())) { - throw new ValidationException( + throw new DataConflictException( "Field: event.state. Error: " + "У события достигнут лимит запросов на участие. " + "Value: " + confirmedRequests @@ -60,7 +61,7 @@ public Request createRequest(Integer userId, Integer eventId) { request.setRequester(user); request.setEvent(event); request.setStatus(RequestStatus.PENDING); - if (!event.getRequestModeration()) { + if (!event.getRequestModeration() || event.getParticipantLimit() == 0) { request.setStatus(RequestStatus.CONFIRMED); } request.setCreated(LocalDateTime.now()); @@ -68,24 +69,28 @@ public Request createRequest(Integer userId, Integer eventId) { } @Override - public List getRequestsByUserId(Integer userId) { - return requestRepository.findAllByRequester_Id(userId); + public List getRequestsByUserId(Integer userId) { + List requests = requestRepository.findAllByRequester_Id(userId); + if (requests.isEmpty()) { + return List.of(); + } + return requests.stream().map(RequestMapper::toRequestDto).toList(); } @Override - public Request deleteRequest(Integer userId, Integer requestId) { + public Request CanceledRequest(Integer userId, Integer requestId) { Request request = requestRepository.findById(requestId) .orElseThrow(() -> new NotFoundException("Не найден запрос id=" + requestId)); if (!request.getRequester().getId().equals(userId)) { throw new ValidationException( "Field: request.requester.id. Error: " + - "Нельзя удалить чужой запрос. " + + "Нельзя отменить чужой запрос. " + "Value: " + request.getRequester().getId() ); } - requestRepository.delete(request); - return request; + request.setStatus(RequestStatus.CANCELED); + return requestRepository.save(request); } /** diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/UserService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/UserService.java index 1f0d474..15ae76e 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/UserService.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/UserService.java @@ -7,6 +7,8 @@ public interface UserService { List getUsers(); + List getUsers(List ids); + User getUserById(Integer id); User addUser(User user); diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/UserServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/UserServiceImpl.java index bfc7e28..917f20a 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/UserServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/UserServiceImpl.java @@ -1,6 +1,7 @@ package ru.practicum.evmsevice.service; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import ru.practicum.evmsevice.exception.NotFoundException; import ru.practicum.evmsevice.model.User; import ru.practicum.evmsevice.repository.UserRepository; @@ -8,6 +9,7 @@ import java.util.List; @Service +@Transactional public class UserServiceImpl implements UserService { private final UserRepository userRepository; @@ -26,6 +28,11 @@ public List getUsers() { return userRepository.findAll(); } + @Override + public List getUsers(List ids) { + return userRepository.findAllById(ids); + } + @Override public User getUserById(Integer id) { User user = userRepository.findById(id) diff --git a/ewm-service/src/main/resources/application.properties b/ewm-service/src/main/resources/application.properties index ce58e1d..10fcf26 100644 --- a/ewm-service/src/main/resources/application.properties +++ b/ewm-service/src/main/resources/application.properties @@ -4,13 +4,8 @@ spring.sql.init.mode=always spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.datasource.driverClassName=org.postgresql.Driver -spring.datasource.url=jdbc:postgresql://192.168.0.102:5434/ewmdb +spring.datasource.url=jdbc:postgresql://localhost:5434/ewmdb spring.datasource.username=ewmdb spring.datasource.password=ewmdb -#spring.datasource.driverClassName=org.h2.Driver -#spring.datasource.url=jdbc:h2:file:C:/dev/data/ewmdb -#spring.datasource.username=ewmdb -#spring.datasource.password=ewmdb - statserver.url=http://localhost:9090 \ No newline at end of file diff --git a/ewm-service/src/main/resources/schema.sql b/ewm-service/src/main/resources/schema.sql index f2538e3..f0abc87 100644 --- a/ewm-service/src/main/resources/schema.sql +++ b/ewm-service/src/main/resources/schema.sql @@ -1,3 +1,12 @@ + +DROP TABLE eventlinks; +DROP TABLE compilations; +DROP TABLE requests; +DROP TABLE events; +DROP TABLE categories; +DROP TABLE users; + + CREATE TABLE IF NOT EXISTS categories ( id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(128) NOT NULL, @@ -7,8 +16,8 @@ CREATE TABLE IF NOT EXISTS categories ( CREATE TABLE IF NOT EXISTS users ( id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, - name VARCHAR(128) NOT NULL, - email VARCHAR(128) NOT NULL, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, CONSTRAINT pk_user PRIMARY KEY (id), CONSTRAINT UQ_USER_EMAIL UNIQUE (email) ); @@ -17,7 +26,6 @@ CREATE TABLE IF NOT EXISTS events ( id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, annotation VARCHAR(2000), category_id INTEGER, - confirmedRequests INTEGER, createdOn TIMESTAMP WITHOUT TIME ZONE, description VARCHAR(7000), eventDate TIMESTAMP WITHOUT TIME ZONE, @@ -51,13 +59,15 @@ CREATE TABLE IF NOT EXISTS compilations ( id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, title VARCHAR(128), pinned BOOLEAN, - CONSTRAINT pk_compilation PRIMARY KEY (id) + CONSTRAINT pk_compilation PRIMARY KEY (id), + CONSTRAINT UQ_COMPILATION_TITLE UNIQUE (title) ); + CREATE TABLE IF NOT EXISTS eventlinks ( event_id INTEGER NOT NULL, - compiation_id INTEGER NOT NULL, - CONSTRAINT pk_eventlinks PRIMARY KEY (event_id, compiation_id), + compilation_id INTEGER NOT NULL, + CONSTRAINT pk_eventlinks PRIMARY KEY (event_id, compilation_id), CONSTRAINT fk_links_to_events FOREIGN KEY (event_id) REFERENCES events (id), - CONSTRAINT fk_links_to_compilations FOREIGN KEY (compiation_id) REFERENCES compilations (id) + CONSTRAINT fk_links_to_compilations FOREIGN KEY (compilation_id) REFERENCES compilations (id) ); diff --git a/stats-server/stat-client/src/main/java/ru/practicum/statclient/BaseClient.java b/stats-server/stat-client/src/main/java/ru/practicum/statclient/BaseClient.java index bfc5406..be8a867 100644 --- a/stats-server/stat-client/src/main/java/ru/practicum/statclient/BaseClient.java +++ b/stats-server/stat-client/src/main/java/ru/practicum/statclient/BaseClient.java @@ -1,12 +1,12 @@ package ru.practicum.statclient; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestTemplate; import ru.practicum.statdto.StatsDto; -import ru.practicum.statdto.StatsDtoList; import java.util.List; import java.util.Map; @@ -71,7 +71,7 @@ protected ResponseEntity makeAndSendRequest(HttpMethod method, protected List getStatsList(String path, Map parameters) { - StatsDtoList serverResponse = new StatsDtoList(); + ResponseEntity> serverResponse = null; try { if (parameters != null) { StringBuilder stringParametrs = new StringBuilder(path); @@ -82,12 +82,15 @@ protected List getStatsList(String path, stringParametrs.append(key); stringParametrs.append("}&"); } - serverResponse = rest.getForObject(stringParametrs.toString(), StatsDtoList.class, parameters); + serverResponse = rest.exchange(stringParametrs.toString(), HttpMethod.GET, null, + new ParameterizedTypeReference>() { + }, parameters); } } catch (Exception e) { e.printStackTrace(); return List.of(); } - return serverResponse.getStatsDtos(); + List dtos = serverResponse.getBody(); + return dtos; } } diff --git a/stats-server/stat-dto/src/main/java/ru/practicum/statdto/StatsDtoList.java b/stats-server/stat-dto/src/main/java/ru/practicum/statdto/StatsDtoList.java deleted file mode 100644 index c2a8440..0000000 --- a/stats-server/stat-dto/src/main/java/ru/practicum/statdto/StatsDtoList.java +++ /dev/null @@ -1,14 +0,0 @@ -package ru.practicum.statdto; - -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.util.List; - -@Setter -@Getter -@NoArgsConstructor -public class StatsDtoList { - private List statsDtos; -} diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/controller/StatController.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/controller/StatController.java index 85165c6..316538b 100644 --- a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/controller/StatController.java +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/controller/StatController.java @@ -6,7 +6,6 @@ import org.springframework.web.bind.annotation.*; import ru.practicum.statdto.HitDto; import ru.practicum.statdto.StatsDto; -import ru.practicum.statdto.StatsDtoList; import ru.practicum.statsvc.service.StatService; import java.util.List; @@ -28,7 +27,7 @@ public void hit(@RequestBody HitDto dto) { @GetMapping("/stats") @ResponseStatus(HttpStatus.OK) - public StatsDtoList getStats( + public List getStats( @RequestParam(required = false) String start, @RequestParam(required = false) String end, @RequestParam(required = false) List uris, @@ -36,8 +35,8 @@ public StatsDtoList getStats( @RequestParam(defaultValue = "10") Integer size) { log.info("Запрашивается информация о посещении эндпоинта {} с {} до {}.", uris, start, end); List statDtos = statService.getStats(start, end, uris, unique, size); - StatsDtoList statsDtoList = new StatsDtoList(); - statsDtoList.setStatsDtos(statDtos); - return statsDtoList; + // StatsDtoList statsDtoList = new StatsDtoList(); + // statsDtoList.setStatsDtos(statDtos); + return statDtos; } } diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/model/ViewStats.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/model/ViewStats.java index 343b3eb..c57f911 100644 --- a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/model/ViewStats.java +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/model/ViewStats.java @@ -10,7 +10,7 @@ @AllArgsConstructor @NoArgsConstructor public class ViewStats { - String app; - String uri; - Integer hits; + private String app; + private String uri; + private Integer hits; } diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatDbStorage.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatDbStorage.java index 6e45900..1960875 100644 --- a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatDbStorage.java +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatDbStorage.java @@ -31,7 +31,7 @@ public StatDbStorage(NamedParameterJdbcTemplate jdbc) { } @Override - public EndpointHit addHit(EndpointHit hit) { + public void addHit(EndpointHit hit) { GeneratedKeyHolder generatedKeyHolder = new GeneratedKeyHolder(); try { jdbc.update(SQL_INSERT_HIT, @@ -49,8 +49,6 @@ public EndpointHit addHit(EndpointHit hit) { // получаем идентификатор final Integer hitId = generatedKeyHolder.getKey().intValue(); hit.setId(hitId); - - return hit; } @Override @@ -94,10 +92,8 @@ public List getViewStats(LocalDateTime start, LocalDateTime end, List sql.append(" LIMIT :size"); } try { - List viewStatsList = jdbc.query(sql.toString(), - parameters, + return jdbc.query(sql.toString(), parameters, new ViewStatsRowMapper()); - return viewStatsList; } catch (EmptyResultDataAccessException ignored) { return List.of(); } diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatStorage.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatStorage.java index 573ee32..886ebd3 100644 --- a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatStorage.java +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatStorage.java @@ -7,7 +7,7 @@ import java.util.List; public interface StatStorage { - EndpointHit addHit(EndpointHit hit); + void addHit(EndpointHit hit); List getViewStats(LocalDateTime start, LocalDateTime end, List uris, Boolean unique, Integer size); } diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/service/StatServiceImpl.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/service/StatServiceImpl.java index dbb381e..345c09a 100644 --- a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/service/StatServiceImpl.java +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/service/StatServiceImpl.java @@ -1,6 +1,7 @@ package ru.practicum.statsvc.service; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import ru.practicum.statdto.HitDto; import ru.practicum.statdto.StatsDto; import ru.practicum.statsvc.exception.ValidationException; @@ -14,6 +15,7 @@ import java.util.List; @Service +@Transactional public class StatServiceImpl implements StatService { private static final DateTimeFormatter DATA_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private final StatDbStorage storage; @@ -38,9 +40,15 @@ public List getStats(String startTxt, try { if (startTxt != null && !startTxt.isEmpty()) { start = LocalDateTime.parse(startTxt, DATA_TIME_FORMATTER); + if (endTxt == null) { + throw new ValidationException("Отсутствует окончание временного периода."); + } } if (endTxt != null && !endTxt.isEmpty()) { end = LocalDateTime.parse(endTxt, DATA_TIME_FORMATTER); + if (start == null) { + throw new ValidationException("Отсутствует начало временного периода."); + } } } catch (DateTimeParseException e) { throw new ValidationException("Некорректный формат времени. " + e.getMessage()); diff --git a/stats-server/stat-svc/src/main/resources/application.properties b/stats-server/stat-svc/src/main/resources/application.properties index a69735a..9099ccc 100644 --- a/stats-server/stat-svc/src/main/resources/application.properties +++ b/stats-server/stat-svc/src/main/resources/application.properties @@ -3,12 +3,8 @@ spring.sql.init.mode=always spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.datasource.driverClassName=org.postgresql.Driver -spring.datasource.url=jdbc:postgresql://192.168.0.102:5432/statdb +spring.datasource.url=jdbc:postgresql://localhost:5432/statdb spring.datasource.username=statdb spring.datasource.password=statdb -#spring.datasource.driverClassName=org.h2.Driver -#spring.datasource.url=jdbc:h2:mem:statdb -#spring.datasource.username=statdb -#spring.datasource.password=statdb diff --git a/stats-server/stat-svc/src/main/resources/schema.sql b/stats-server/stat-svc/src/main/resources/schema.sql index 431f17f..c1e9dff 100644 --- a/stats-server/stat-svc/src/main/resources/schema.sql +++ b/stats-server/stat-svc/src/main/resources/schema.sql @@ -1,3 +1,5 @@ +DROP TABLE endpointhits; + CREATE TABLE IF NOT EXISTS endpointhits ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, From 1d6477e03fd517b85bc3310d842374ba821d6962 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Tue, 17 Jun 2025 00:05:50 +0700 Subject: [PATCH 21/29] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE?= =?UTF-8?q?=D0=BA=20=D1=81=D1=82=D0=B8=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../evmsevice/client/StatsClient.java | 2 - .../evmsevice/controller/AdminController.java | 7 +- .../evmsevice/controller/ErrorAdvisor.java | 6 -- .../controller/PublicController.java | 14 ++-- .../evmsevice/controller/UserController.java | 4 +- .../evmsevice/dto/CompilationDto.java | 1 - .../practicum/evmsevice/dto/NewEventDto.java | 3 +- .../evmsevice/dto/PatchCompilationDto.java | 2 - .../evmsevice/dto/RequestUpdateDto.java | 5 +- .../evmsevice/dto/UpdateEventUserRequest.java | 12 ++- .../evmsevice/mapper/CategoryMapper.java | 3 +- .../evmsevice/mapper/CompilationMapper.java | 3 +- .../evmsevice/mapper/EventMapper.java | 17 +++-- .../evmsevice/mapper/RequestMapper.java | 4 +- .../evmsevice/mapper/UserMapper.java | 5 +- .../practicum/evmsevice/model/Category.java | 5 +- .../evmsevice/model/Compilation.java | 6 +- .../ru/practicum/evmsevice/model/Event.java | 5 +- .../ru/practicum/evmsevice/model/User.java | 5 +- .../repository/CategoryRepository.java | 2 - .../repository/CompilationRepository.java | 2 +- .../evmsevice/repository/EventRepository.java | 3 +- .../repository/EventSpecification.java | 3 - .../repository/RequestRepository.java | 3 +- .../evmsevice/repository/UserRepository.java | 2 - .../evmsevice/service/EventService.java | 1 - .../evmsevice/service/RequestService.java | 4 +- .../src/main/resources/application.properties | 2 - ewm-service/src/main/resources/schema.sql | 73 ++++++++++--------- .../src/main/resources/application.properties | 1 - 30 files changed, 98 insertions(+), 107 deletions(-) diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java b/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java index 176eab7..edbc94d 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java @@ -14,8 +14,6 @@ import ru.practicum.statdto.StatsDto; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java index 0ebc957..49fecb3 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java @@ -12,7 +12,6 @@ import ru.practicum.evmsevice.mapper.CategoryMapper; import ru.practicum.evmsevice.mapper.UserMapper; import ru.practicum.evmsevice.model.Category; -import ru.practicum.evmsevice.model.Event; import ru.practicum.evmsevice.model.User; import ru.practicum.evmsevice.service.CategoryService; import ru.practicum.evmsevice.service.CompilationService; @@ -29,13 +28,13 @@ @RestController @RequestMapping("/admin") public class AdminController { - @Value("${spring.application.name}") - private String appName; private final StatsClient statsClient; private final UserService userService; private final CategoryService categoryService; private final EventService eventService; private final CompilationService compilationService; + @Value("${spring.application.name}") + private String appName; @GetMapping("/users") @ResponseStatus(HttpStatus.OK) @@ -129,7 +128,7 @@ public List findEvents( HttpServletRequest request) { log.info("Администратор запрашивает список событий. users:{}, states:{}, categories:{} rangeStart:{}, rangeStart:{}.", users, states, categories, rangeStart, rangeEnd); - return eventService.findEventsByAdmin(states, users, categories, rangeStart, rangeEnd, from, size ); + return eventService.findEventsByAdmin(states, users, categories, rangeStart, rangeEnd, from, size); } @PatchMapping("/events/{eventId}") diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java index 61bd037..579b712 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java @@ -4,22 +4,16 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; -import org.springframework.validation.FieldError; -import org.springframework.validation.ObjectError; -import org.springframework.web.ErrorResponse; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.method.annotation.HandlerMethodValidationException; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import ru.practicum.evmsevice.dto.ApiError; import ru.practicum.evmsevice.exception.*; import java.time.LocalDateTime; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; /** diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java index 248983b..2b373a2 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java @@ -1,8 +1,6 @@ package ru.practicum.evmsevice.controller; import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.Valid; -import jakarta.validation.constraints.Pattern; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -29,12 +27,12 @@ @RequestMapping() public class PublicController { private static final String RGEXP_DATE_TIME = "yyyy-MM-dd' 'HH:mm:ss"; - @Value("${spring.application.name}") - private String appName; private final StatsClient statsClient; private final EventService eventService; private final CompilationService compilationService; private final CategoryService categoryService; + @Value("${spring.application.name}") + private String appName; @GetMapping("/events/{id}") @ResponseStatus(HttpStatus.OK) @@ -58,11 +56,11 @@ public List findAllEvents( @RequestParam(name = "categories", required = false) List categories, @RequestParam(name = "paid", required = false) Boolean paid, @RequestParam(name = "rangeStart", required = false) - //@Valid @Pattern(regexp = RGEXP_DATE_TIME, message = "Не верный формат даты.") - String rangeStart, + //@Valid @Pattern(regexp = RGEXP_DATE_TIME, message = "Не верный формат даты.") + String rangeStart, @RequestParam(name = "rangeEnd", required = false) - //@Valid @Pattern(regexp = RGEXP_DATE_TIME, message = "Не верный формат даты.") - String rangeEnd, + //@Valid @Pattern(regexp = RGEXP_DATE_TIME, message = "Не верный формат даты.") + String rangeEnd, @RequestParam(name = "onlyAvailable", defaultValue = "false") Boolean onlyAvailable, @RequestParam(name = "sort", defaultValue = "EVENT_DATE") String sort, @RequestParam(name = "from", defaultValue = "0") Integer from, diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java index 6bfefbf..48a5dd9 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java @@ -53,7 +53,7 @@ public List getEvents(@PathVariable int userId, public EventFullDto updateEvent(@PathVariable Integer userId, @PathVariable Integer eventId, @Validated @RequestBody UpdateEventUserRequest eventDto) { - log.info("Пользователь id={} изменяет информацию об инциированном событии. {}" , userId, eventDto.toString()); + log.info("Пользователь id={} изменяет информацию об инциированном событии. {}", userId, eventDto.toString()); return eventService.patchEvent(eventId, eventDto, userId); } @@ -99,7 +99,7 @@ public List findRequestsByUserId(@PathVariable Integer userId) { @PatchMapping("/{userId}/requests/{requestId}/cancel") @ResponseStatus(HttpStatus.OK) public RequestDto canceledRequestById(@PathVariable Integer userId, - @PathVariable Integer requestId) { + @PathVariable Integer requestId) { log.info("Пользователь id={} отменяет запрос id={}.", userId, requestId); return RequestMapper.toRequestDto(requestService.CanceledRequest(userId, requestId)); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CompilationDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CompilationDto.java index 4330031..d292b6a 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CompilationDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CompilationDto.java @@ -4,7 +4,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import ru.practicum.evmsevice.model.Event; import java.util.ArrayList; import java.util.List; diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewEventDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewEventDto.java index d24dcf2..c350c20 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewEventDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewEventDto.java @@ -14,8 +14,7 @@ @Getter @NoArgsConstructor @ToString -public class NewEventDto -{ +public class NewEventDto { @NotEmpty(message = "Аннотация не может быть пустой.") @NotBlank(message = "Аннотация не может быть пустой.") @Size(min = 20, max = 2000, message = "длина аннотации 20 - 2000 символов.") diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/PatchCompilationDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/PatchCompilationDto.java index 0ca90cb..8ffe0af 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/PatchCompilationDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/PatchCompilationDto.java @@ -1,7 +1,5 @@ package ru.practicum.evmsevice.dto; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestUpdateDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestUpdateDto.java index 544cc0a..da36867 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestUpdateDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/RequestUpdateDto.java @@ -1,6 +1,9 @@ package ru.practicum.evmsevice.dto; -import lombok.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; import ru.practicum.evmsevice.enums.RequestStatus; import java.util.ArrayList; diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UpdateEventUserRequest.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UpdateEventUserRequest.java index 76eb925..d8d8627 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UpdateEventUserRequest.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/UpdateEventUserRequest.java @@ -1,9 +1,13 @@ package ru.practicum.evmsevice.dto; import com.fasterxml.jackson.annotation.JsonFormat; -import jakarta.validation.constraints.*; -import lombok.*; -import ru.practicum.evmsevice.enums.EventState; +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; import ru.practicum.evmsevice.enums.EventUserAction; import ru.practicum.evmsevice.model.Location; @@ -13,7 +17,7 @@ @Getter @NoArgsConstructor @ToString -public class UpdateEventUserRequest{ +public class UpdateEventUserRequest { @Size(min = 20, max = 2000, message = "длина аннотации 20 - 2000 символов.") private String annotation; private Integer category; diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CategoryMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CategoryMapper.java index 3f7cc76..3143fe0 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CategoryMapper.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CategoryMapper.java @@ -5,7 +5,8 @@ import ru.practicum.evmsevice.model.Category; public class CategoryMapper { - private CategoryMapper() {} + private CategoryMapper() { + } public static Category toCategory(NewCategoryDto dto) { Category category = new Category(); diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CompilationMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CompilationMapper.java index 460e32f..27f1b3b 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CompilationMapper.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CompilationMapper.java @@ -8,7 +8,8 @@ import java.util.List; public class CompilationMapper { - private CompilationMapper() {} + private CompilationMapper() { + } public static Compilation toCompilation(NewCompilationDto dto) { Compilation c = new Compilation(); diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java index fa59191..6526d08 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/EventMapper.java @@ -3,14 +3,15 @@ import ru.practicum.evmsevice.dto.EventFullDto; import ru.practicum.evmsevice.dto.EventShortDto; import ru.practicum.evmsevice.dto.NewEventDto; -import ru.practicum.evmsevice.model.Event; import ru.practicum.evmsevice.enums.EventState; +import ru.practicum.evmsevice.model.Event; import ru.practicum.evmsevice.model.Location; import java.time.LocalDateTime; public class EventMapper { - private EventMapper() {} + private EventMapper() { + } public static Event toEvent(final NewEventDto newDto) { Event event = new Event(); @@ -21,11 +22,11 @@ public static Event toEvent(final NewEventDto newDto) { event.setLat(newDto.getLocation().getLat()); event.setLon(newDto.getLocation().getLon()); event.setPaid(false); - if(newDto.getPaid() != null) { + if (newDto.getPaid() != null) { event.setPaid(newDto.getPaid()); } event.setParticipantLimit(0); - if(newDto.getParticipantLimit() != null) { + if (newDto.getParticipantLimit() != null) { event.setParticipantLimit(newDto.getParticipantLimit()); } event.setRequestModeration(true); @@ -50,7 +51,7 @@ public static EventFullDto toFullDto(Event event) { dto.setInitiator(UserMapper.toUserShortDto(event.getInitiator())); dto.setLocation(new Location(event.getLat(), event.getLon())); dto.setParticipantLimit(0); - if(event.getParticipantLimit() != null) { + if (event.getParticipantLimit() != null) { dto.setParticipantLimit(event.getParticipantLimit()); } dto.setRequestModeration(event.getRequestModeration()); @@ -63,7 +64,7 @@ public static EventFullDto toFullDto(Event event) { dto.setConfirmedRequests(event.getConfirmedRequests()); } dto.setViews(0); - if(event.getViews() != null) { + if (event.getViews() != null) { dto.setViews(event.getViews()); } return dto; @@ -79,7 +80,7 @@ public static EventShortDto toShortDto(Event event) { dto.setEventDate(event.getEventDate()); dto.setPaid(event.getPaid()); dto.setParticipantLimit(0); - if(event.getParticipantLimit() != null) { + if (event.getParticipantLimit() != null) { dto.setParticipantLimit(event.getParticipantLimit()); } dto.setConfirmedRequests(0); @@ -87,7 +88,7 @@ public static EventShortDto toShortDto(Event event) { dto.setConfirmedRequests(event.getConfirmedRequests()); } dto.setViews(0); - if(event.getViews() != null) { + if (event.getViews() != null) { dto.setViews(event.getViews()); } return dto; diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/RequestMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/RequestMapper.java index a6ac9ff..8323551 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/RequestMapper.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/RequestMapper.java @@ -4,7 +4,9 @@ import ru.practicum.evmsevice.model.Request; public class RequestMapper { - private RequestMapper() {} + private RequestMapper() { + } + public static RequestDto toRequestDto(Request request) { RequestDto requestDto = new RequestDto(); requestDto.setId(request.getId()); diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/UserMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/UserMapper.java index fda352f..a54b7b6 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/UserMapper.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/UserMapper.java @@ -5,7 +5,9 @@ import ru.practicum.evmsevice.model.User; public class UserMapper { - private UserMapper() {} + private UserMapper() { + } + public static User toUser(UserDto dto) { User user = new User(); if (dto.getId() != null) { @@ -15,6 +17,7 @@ public static User toUser(UserDto dto) { user.setEmail(dto.getEmail()); return user; } + public static UserDto toUserDto(User user) { UserDto dto = new UserDto(); dto.setId(user.getId()); diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Category.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Category.java index e8e071e..65180a3 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Category.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Category.java @@ -2,7 +2,10 @@ import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; -import lombok.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; @Entity @Setter diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Compilation.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Compilation.java index 8d7d04f..4369bd2 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Compilation.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Compilation.java @@ -5,9 +5,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; -import java.util.ArrayList; import java.util.HashSet; -import java.util.List; import java.util.Set; @Entity @@ -21,8 +19,8 @@ public class Compilation { private Integer id; @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.REFRESH) @JoinTable(name = "eventlinks", - joinColumns = { @JoinColumn(name = "compilation_id") }, - inverseJoinColumns = { @JoinColumn(name = "event_id") }) + joinColumns = {@JoinColumn(name = "compilation_id")}, + inverseJoinColumns = {@JoinColumn(name = "event_id")}) private Set events = new HashSet<>(); //private List events = new ArrayList<>(); @Column(name = "title", nullable = false) diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java index 22a67b6..7048508 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Event.java @@ -1,15 +1,12 @@ package ru.practicum.evmsevice.model; -import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.Getter; -import lombok.Setter; import lombok.NoArgsConstructor; +import lombok.Setter; import ru.practicum.evmsevice.enums.EventState; import java.time.LocalDateTime; -import java.util.HashSet; -import java.util.Set; @Entity @Setter diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/User.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/User.java index 6726fc0..390b7a7 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/model/User.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/User.java @@ -1,7 +1,10 @@ package ru.practicum.evmsevice.model; import jakarta.persistence.*; -import lombok.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; /** * Класс описания пользователя diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CategoryRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CategoryRepository.java index 61f4d91..0775f42 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CategoryRepository.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CategoryRepository.java @@ -3,7 +3,5 @@ import org.springframework.data.jpa.repository.JpaRepository; import ru.practicum.evmsevice.model.Category; -import java.util.Optional; - public interface CategoryRepository extends JpaRepository { } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CompilationRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CompilationRepository.java index 8450b70..7a76ff3 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CompilationRepository.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CompilationRepository.java @@ -5,6 +5,6 @@ import java.util.List; -public interface CompilationRepository extends JpaRepository { +public interface CompilationRepository extends JpaRepository { List findAllByPinnedEquals(boolean pinned); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java index 9b66dca..be0b000 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java @@ -1,6 +1,5 @@ package ru.practicum.evmsevice.repository; -import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import ru.practicum.evmsevice.model.Event; @@ -8,7 +7,7 @@ import java.util.List; public interface EventRepository extends JpaRepository, - JpaSpecificationExecutor { + JpaSpecificationExecutor { List findEventsByInitiator_Id(int id); List findEventsByIdIn(List ids); diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventSpecification.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventSpecification.java index 83134d6..daf8157 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventSpecification.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventSpecification.java @@ -1,12 +1,9 @@ package ru.practicum.evmsevice.repository; import org.springframework.data.jpa.domain.Specification; -import ru.practicum.evmsevice.enums.EventState; import ru.practicum.evmsevice.model.Event; -import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.Date; import java.util.List; public class EventSpecification { diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java index a51f49f..97cf0f7 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/RequestRepository.java @@ -3,13 +3,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import ru.practicum.evmsevice.enums.RequestStatus; import ru.practicum.evmsevice.model.EventConfirmedRequestCount; import ru.practicum.evmsevice.model.Request; import java.util.List; -public interface RequestRepository extends JpaRepository { +public interface RequestRepository extends JpaRepository { List findAllByRequester_Id(int userId); List findAllByEvent_Id(int eventId); diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/UserRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/UserRepository.java index 4f06cc2..5887768 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/UserRepository.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/UserRepository.java @@ -3,7 +3,5 @@ import org.springframework.data.jpa.repository.JpaRepository; import ru.practicum.evmsevice.model.User; -import java.util.List; - public interface UserRepository extends JpaRepository { } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java index cab08ba..531ee7a 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventService.java @@ -1,6 +1,5 @@ package ru.practicum.evmsevice.service; -import org.springframework.web.bind.annotation.RequestParam; import ru.practicum.evmsevice.dto.*; import ru.practicum.evmsevice.model.Event; diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java index d8a1ed1..ea8f2cb 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java @@ -3,14 +3,12 @@ import ru.practicum.evmsevice.dto.RequestDto; import ru.practicum.evmsevice.dto.RequestGroupDto; import ru.practicum.evmsevice.dto.RequestUpdateDto; -import ru.practicum.evmsevice.enums.RequestStatus; -import ru.practicum.evmsevice.model.Event; import ru.practicum.evmsevice.model.Request; import java.util.List; public interface RequestService { - Request createRequest(Integer userId, Integer eventId ); + Request createRequest(Integer userId, Integer eventId); List getRequestsByUserId(Integer userId); diff --git a/ewm-service/src/main/resources/application.properties b/ewm-service/src/main/resources/application.properties index 10fcf26..73b37e8 100644 --- a/ewm-service/src/main/resources/application.properties +++ b/ewm-service/src/main/resources/application.properties @@ -2,10 +2,8 @@ server.port=8080 spring.application.name=ewm-service spring.sql.init.mode=always spring.jackson.date-format=yyyy-MM-dd HH:mm:ss - spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.url=jdbc:postgresql://localhost:5434/ewmdb spring.datasource.username=ewmdb spring.datasource.password=ewmdb - statserver.url=http://localhost:9090 \ No newline at end of file diff --git a/ewm-service/src/main/resources/schema.sql b/ewm-service/src/main/resources/schema.sql index f0abc87..a193bd4 100644 --- a/ewm-service/src/main/resources/schema.sql +++ b/ewm-service/src/main/resources/schema.sql @@ -1,4 +1,3 @@ - DROP TABLE eventlinks; DROP TABLE compilations; DROP TABLE requests; @@ -7,65 +6,71 @@ DROP TABLE categories; DROP TABLE users; -CREATE TABLE IF NOT EXISTS categories ( - id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, - name VARCHAR(128) NOT NULL, +CREATE TABLE IF NOT EXISTS categories +( + id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, + name VARCHAR(128) NOT NULL, CONSTRAINT pk_category PRIMARY KEY (id), CONSTRAINT UQ_CATEGORY UNIQUE (name) ); -CREATE TABLE IF NOT EXISTS users ( - id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, - name VARCHAR(255) NOT NULL, - email VARCHAR(255) NOT NULL, +CREATE TABLE IF NOT EXISTS users +( + id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, CONSTRAINT pk_user PRIMARY KEY (id), CONSTRAINT UQ_USER_EMAIL UNIQUE (email) ); -CREATE TABLE IF NOT EXISTS events ( - id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, - annotation VARCHAR(2000), - category_id INTEGER, - createdOn TIMESTAMP WITHOUT TIME ZONE, - description VARCHAR(7000), - eventDate TIMESTAMP WITHOUT TIME ZONE, - initiator_id INTEGER NOT NULL, - lat FLOAT, - lon FLOAT, - paid BOOLEAN, - participantLimit INTEGER, - publishedOn TIMESTAMP WITHOUT TIME ZONE, +CREATE TABLE IF NOT EXISTS events +( + id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, + annotation VARCHAR(2000), + category_id INTEGER, + createdOn TIMESTAMP WITHOUT TIME ZONE, + description VARCHAR(7000), + eventDate TIMESTAMP WITHOUT TIME ZONE, + initiator_id INTEGER NOT NULL, + lat FLOAT, + lon FLOAT, + paid BOOLEAN, + participantLimit INTEGER, + publishedOn TIMESTAMP WITHOUT TIME ZONE, requestModeration BOOLEAN, - state VARCHAR(32), - title VARCHAR(128), + state VARCHAR(32), + title VARCHAR(128), CONSTRAINT pk_event PRIMARY KEY (id), CONSTRAINT fk_events_to_users FOREIGN KEY (initiator_id) REFERENCES users (id), CONSTRAINT fk_events_to_categories FOREIGN KEY (category_id) REFERENCES categories (id) ); -CREATE TABLE IF NOT EXISTS requests ( - id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, - requester_id INTEGER NOT NULL, - event_id INTEGER NOT NULL, - status VARCHAR(32), - created TIMESTAMP WITHOUT TIME ZONE, +CREATE TABLE IF NOT EXISTS requests +( + id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, + requester_id INTEGER NOT NULL, + event_id INTEGER NOT NULL, + status VARCHAR(32), + created TIMESTAMP WITHOUT TIME ZONE, CONSTRAINT pk_request PRIMARY KEY (id), CONSTRAINT fk_requests_to_users FOREIGN KEY (requester_id) REFERENCES users (id), CONSTRAINT fk_requests_to_events FOREIGN KEY (event_id) REFERENCES events (id), CONSTRAINT unique_requester_event UNIQUE (requester_id, event_id) ); -CREATE TABLE IF NOT EXISTS compilations ( - id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, - title VARCHAR(128), +CREATE TABLE IF NOT EXISTS compilations +( + id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, + title VARCHAR(128), pinned BOOLEAN, CONSTRAINT pk_compilation PRIMARY KEY (id), CONSTRAINT UQ_COMPILATION_TITLE UNIQUE (title) ); -CREATE TABLE IF NOT EXISTS eventlinks ( - event_id INTEGER NOT NULL, +CREATE TABLE IF NOT EXISTS eventlinks +( + event_id INTEGER NOT NULL, compilation_id INTEGER NOT NULL, CONSTRAINT pk_eventlinks PRIMARY KEY (event_id, compilation_id), CONSTRAINT fk_links_to_events FOREIGN KEY (event_id) REFERENCES events (id), diff --git a/stats-server/stat-svc/src/main/resources/application.properties b/stats-server/stat-svc/src/main/resources/application.properties index 9099ccc..4ab67c9 100644 --- a/stats-server/stat-svc/src/main/resources/application.properties +++ b/stats-server/stat-svc/src/main/resources/application.properties @@ -1,7 +1,6 @@ server.port=9090 spring.sql.init.mode=always spring.jackson.date-format=yyyy-MM-dd HH:mm:ss - spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.url=jdbc:postgresql://localhost:5432/statdb spring.datasource.username=statdb From 1a7fbca7b97ba73c5f7452e415aff1f993dbd912 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Tue, 17 Jun 2025 00:09:49 +0700 Subject: [PATCH 22/29] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE?= =?UTF-8?q?=D0=BA=20=D1=81=D1=82=D0=B8=D0=BB=D1=8F=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ru/practicum/evmsevice/service/RequestService.java | 2 +- .../java/ru/practicum/evmsevice/service/RequestServiceImpl.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java index ea8f2cb..cab08e2 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestService.java @@ -12,7 +12,7 @@ public interface RequestService { List getRequestsByUserId(Integer userId); - Request CanceledRequest(Integer userId, Integer requestId); + Request canceledRequest(Integer userId, Integer requestId); List getRequestsByEventId(Integer userId, Integer eventId); diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java index de595f2..776176e 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java @@ -78,7 +78,7 @@ public List getRequestsByUserId(Integer userId) { } @Override - public Request CanceledRequest(Integer userId, Integer requestId) { + public Request canceledRequest(Integer userId, Integer requestId) { Request request = requestRepository.findById(requestId) .orElseThrow(() -> new NotFoundException("Не найден запрос id=" + requestId)); From 01469031d2c693333cd4aff871614ef3f24c1fbc Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Tue, 17 Jun 2025 00:11:11 +0700 Subject: [PATCH 23/29] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE?= =?UTF-8?q?=D0=BA=20=D1=81=D1=82=D0=B8=D0=BB=D1=8F=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ru/practicum/evmsevice/controller/UserController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java index 48a5dd9..6d84676 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java @@ -101,6 +101,6 @@ public List findRequestsByUserId(@PathVariable Integer userId) { public RequestDto canceledRequestById(@PathVariable Integer userId, @PathVariable Integer requestId) { log.info("Пользователь id={} отменяет запрос id={}.", userId, requestId); - return RequestMapper.toRequestDto(requestService.CanceledRequest(userId, requestId)); + return RequestMapper.toRequestDto(requestService.canceledRequest(userId, requestId)); } } From 42eb6c54e7dca764b779a47aa67f39e334aa1505 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Tue, 17 Jun 2025 20:37:59 +0700 Subject: [PATCH 24/29] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE?= =?UTF-8?q?=D0=BA=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 2 +- .../java/ru/practicum/evmsevice/controller/UserController.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8ccf9e7..966757e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,7 +40,7 @@ services: - stats-server environment: - STATSERVER_URL=http://stats-server:9090 - - SPRING_DATASOURCE_URL=jdbc:postgresql://ewm-db:5432/ewmdb + - SPRING_DATASOURCE_URL=jdbc:postgresql://ewm-db:5434/ewmdb - SPRING_DATASOURCE_USERNAME=ewmdb - SPRING_DATASOURCE_PASSWORD=ewmdb diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java index 6d84676..bad73df 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java @@ -53,7 +53,7 @@ public List getEvents(@PathVariable int userId, public EventFullDto updateEvent(@PathVariable Integer userId, @PathVariable Integer eventId, @Validated @RequestBody UpdateEventUserRequest eventDto) { - log.info("Пользователь id={} изменяет информацию об инциированном событии. {}", userId, eventDto.toString()); + log.info("Пользователь id={} изменяет информацию об инициированном событии. {}", userId, eventDto.toString()); return eventService.patchEvent(eventId, eventDto, userId); } From 8ae74314e2b4ae40e59dfeaf20045bfdefc9a128 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Tue, 17 Jun 2025 20:59:58 +0700 Subject: [PATCH 25/29] fix: schema.sql --- ewm-service/src/main/resources/schema.sql | 8 -------- stats-server/stat-svc/src/main/resources/schema.sql | 2 -- 2 files changed, 10 deletions(-) diff --git a/ewm-service/src/main/resources/schema.sql b/ewm-service/src/main/resources/schema.sql index a193bd4..8c871d8 100644 --- a/ewm-service/src/main/resources/schema.sql +++ b/ewm-service/src/main/resources/schema.sql @@ -1,11 +1,3 @@ -DROP TABLE eventlinks; -DROP TABLE compilations; -DROP TABLE requests; -DROP TABLE events; -DROP TABLE categories; -DROP TABLE users; - - CREATE TABLE IF NOT EXISTS categories ( id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, diff --git a/stats-server/stat-svc/src/main/resources/schema.sql b/stats-server/stat-svc/src/main/resources/schema.sql index c1e9dff..431f17f 100644 --- a/stats-server/stat-svc/src/main/resources/schema.sql +++ b/stats-server/stat-svc/src/main/resources/schema.sql @@ -1,5 +1,3 @@ -DROP TABLE endpointhits; - CREATE TABLE IF NOT EXISTS endpointhits ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, From c7ae70575535cc04a3036c3c5b5c491387362f65 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Tue, 17 Jun 2025 20:59:58 +0700 Subject: [PATCH 26/29] fix: docker-compose.yml --- docker-compose.yml | 8 ++------ ewm-service/src/main/resources/application.properties | 2 +- .../ru/practicum/statsvc/controller/StatController.java | 6 +----- .../stat-svc/src/main/resources/application.properties | 2 +- 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 966757e..38bd919 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,8 +4,6 @@ services: container_name: postgres-stat ports: - "5432:5432" - volumes: - - ./volumes/stat/postgres:/var/lib/postgresql/data/ environment: - POSTGRES_PASSWORD=statdb - POSTGRES_USER=statdb @@ -25,7 +23,7 @@ services: depends_on: - stats-db environment: - - SPRING_DATASOURCE_URL=jdbc:postgresql://stats-db:5432/statdb + - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres-stat:5432/statdb - SPRING_DATASOURCE_USERNAME=statdb - SPRING_DATASOURCE_PASSWORD=statdb @@ -40,7 +38,7 @@ services: - stats-server environment: - STATSERVER_URL=http://stats-server:9090 - - SPRING_DATASOURCE_URL=jdbc:postgresql://ewm-db:5434/ewmdb + - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres-ewm:5434/ewmdb - SPRING_DATASOURCE_USERNAME=ewmdb - SPRING_DATASOURCE_PASSWORD=ewmdb @@ -49,8 +47,6 @@ services: container_name: postgres-ewm ports: - "5434:5432" - volumes: - - ./volumes/ewm/postgres:/var/lib/postgresql/data/ environment: - POSTGRES_PASSWORD=ewmdb - POSTGRES_USER=ewmdb diff --git a/ewm-service/src/main/resources/application.properties b/ewm-service/src/main/resources/application.properties index 73b37e8..5c56ef3 100644 --- a/ewm-service/src/main/resources/application.properties +++ b/ewm-service/src/main/resources/application.properties @@ -3,7 +3,7 @@ spring.application.name=ewm-service spring.sql.init.mode=always spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.datasource.driverClassName=org.postgresql.Driver -spring.datasource.url=jdbc:postgresql://localhost:5434/ewmdb +spring.datasource.url=jdbc:postgresql://192.168.0.102:5434/ewmdb spring.datasource.username=ewmdb spring.datasource.password=ewmdb statserver.url=http://localhost:9090 \ No newline at end of file diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/controller/StatController.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/controller/StatController.java index 316538b..908889f 100644 --- a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/controller/StatController.java +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/controller/StatController.java @@ -15,7 +15,6 @@ @RestController @RequestMapping public class StatController { - private final StatService statService; @PostMapping("/hit") @@ -34,9 +33,6 @@ public List getStats( @RequestParam(defaultValue = "false") Boolean unique, @RequestParam(defaultValue = "10") Integer size) { log.info("Запрашивается информация о посещении эндпоинта {} с {} до {}.", uris, start, end); - List statDtos = statService.getStats(start, end, uris, unique, size); - // StatsDtoList statsDtoList = new StatsDtoList(); - // statsDtoList.setStatsDtos(statDtos); - return statDtos; + return statService.getStats(start, end, uris, unique, size); } } diff --git a/stats-server/stat-svc/src/main/resources/application.properties b/stats-server/stat-svc/src/main/resources/application.properties index 4ab67c9..7d9e7f5 100644 --- a/stats-server/stat-svc/src/main/resources/application.properties +++ b/stats-server/stat-svc/src/main/resources/application.properties @@ -2,7 +2,7 @@ server.port=9090 spring.sql.init.mode=always spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.datasource.driverClassName=org.postgresql.Driver -spring.datasource.url=jdbc:postgresql://localhost:5432/statdb +spring.datasource.url=jdbc:postgresql://192.168.0.102:5432/statdb spring.datasource.username=statdb spring.datasource.password=statdb From 69985d35c50abe08819eb7e2507604e2709b6091 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Thu, 3 Jul 2025 22:10:17 +0700 Subject: [PATCH 27/29] fix: docker-compose.yml 2 --- docker-compose.yml | 37 +++++++++---------- ewm-service/pom.xml | 12 ++---- .../src/main/resources/application.properties | 2 +- 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 38bd919..7779ce4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,20 @@ +version: '3.1' services: + stats-server: + build: stats-server + image: stats-server + container_name: stats-server + ports: + - "9090:9090" + depends_on: + - stats-db + environment: + - SPRING_DATASOURCE_URL=jdbc:postgresql://stats-db:5432/statdb + - SPRING_DATASOURCE_USERNAME=statdb + - SPRING_DATASOURCE_PASSWORD=statdb + stats-db: - image: postgres:16.1 + image: postgres:14-alpine container_name: postgres-stat ports: - "5432:5432" @@ -14,19 +28,6 @@ services: interval: 5s retries: 10 - stats-server: - build: stats-server - image: stats-server - container_name: stats-server - ports: - - "9090:9090" - depends_on: - - stats-db - environment: - - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres-stat:5432/statdb - - SPRING_DATASOURCE_USERNAME=statdb - - SPRING_DATASOURCE_PASSWORD=statdb - ewm-service: build: ewm-service image: ewm-service @@ -35,15 +36,14 @@ services: - "8080:8080" depends_on: - ewm-db - - stats-server environment: - - STATSERVER_URL=http://stats-server:9090 - - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres-ewm:5434/ewmdb + - SPRING_DATASOURCE_URL=jdbc:postgresql://ewm-db:5432/ewmdb - SPRING_DATASOURCE_USERNAME=ewmdb - SPRING_DATASOURCE_PASSWORD=ewmdb + - STATSERVER_URL=http://stats-server:9090 ewm-db: - image: postgres:16.1 + image: postgres:14-alpine container_name: postgres-ewm ports: - "5434:5432" @@ -56,4 +56,3 @@ services: timeout: 5s interval: 5s retries: 10 - diff --git a/ewm-service/pom.xml b/ewm-service/pom.xml index d029e3e..0cadd0b 100644 --- a/ewm-service/pom.xml +++ b/ewm-service/pom.xml @@ -14,17 +14,17 @@ org.springframework.boot - spring-boot-starter-web + spring-boot-starter-actuator org.springframework.boot - spring-boot-starter-validation + spring-boot-starter-web org.springframework.boot - spring-boot-starter-actuator + spring-boot-starter-validation @@ -57,12 +57,6 @@ runtime - - com.h2database - h2 - runtime - - org.springframework.boot spring-boot-starter-data-jpa diff --git a/ewm-service/src/main/resources/application.properties b/ewm-service/src/main/resources/application.properties index 5c56ef3..4781508 100644 --- a/ewm-service/src/main/resources/application.properties +++ b/ewm-service/src/main/resources/application.properties @@ -1,9 +1,9 @@ server.port=8080 -spring.application.name=ewm-service spring.sql.init.mode=always spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.url=jdbc:postgresql://192.168.0.102:5434/ewmdb spring.datasource.username=ewmdb spring.datasource.password=ewmdb +spring.application.name=ewm-service statserver.url=http://localhost:9090 \ No newline at end of file From e25ab72c35cf102faa7deee6ee18974069d5ecf3 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Mon, 7 Jul 2025 21:12:31 +0700 Subject: [PATCH 28/29] =?UTF-8?q?fix:=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=20=D1=81=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=BE?= =?UTF-8?q?=D0=BC=20=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=81=D1=82=D0=B8=D0=BA?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../evmsevice/client/StatsClient.java | 25 ++++-------- .../evmsevice/controller/AdminController.java | 39 ++++++------------- .../evmsevice/controller/ErrorAdvisor.java | 13 +++---- .../controller/PublicController.java | 9 ++--- .../evmsevice/controller/UserController.java | 2 +- .../evmsevice/service/EventServiceImpl.java | 28 ++++++------- .../statsvc/repository/StatDbStorage.java | 24 ++++++------ 7 files changed, 58 insertions(+), 82 deletions(-) diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java b/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java index edbc94d..589c74c 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java @@ -1,6 +1,5 @@ package ru.practicum.evmsevice.client; -import jakarta.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.client.RestTemplateBuilder; @@ -17,17 +16,11 @@ import java.util.List; import java.util.Map; -// class StatsDtoListTypeToken extends TypeToken> { -// } - @Component public class StatsClient extends BaseClient { private static final String PREFIX_HIT = "/hit"; private static final String PREFIX_STATS = "/stats"; - private static final String PREFIX_EVETS = "/events/"; - // private static Gson gson = new GsonBuilder() - // .setPrettyPrinting() - // .create(); + private static final String PREFIX_EVENTS = "/events/"; @Autowired public StatsClient(@Value("${statserver.url}") String serverUrl, RestTemplateBuilder builder) { @@ -47,34 +40,32 @@ public ResponseEntity get(Map parameters) { return makeAndSendRequest(HttpMethod.GET, PREFIX_STATS, parameters, null); } - public void hitInfo(String appName, HttpServletRequest request) { + public void hitInfo(String appName, String uri, String ip) { HitDto hitDto = new HitDto(); hitDto.setApp(appName); - hitDto.setUri(request.getRequestURI()); - hitDto.setIp(request.getRemoteAddr()); + hitDto.setUri(uri); + hitDto.setIp(ip); hitDto.setTimestamp(LocalDateTime.now()); post(hitDto); } - public Integer getEventViews(Integer eventId, Boolean unique) { - Map parameters = Map.of("uris", PREFIX_EVETS + eventId, + Map parameters = Map.of("uris", PREFIX_EVENTS + eventId, "unique", unique); List dtos = getStatsList(PREFIX_STATS, parameters); if (dtos.isEmpty()) { return 0; } - return dtos.get(0).getHits(); + return dtos.getFirst().getHits(); } public List getEventViewsByUris(List eventUris, Boolean unique) { - StringBuilder urisBuilder = new StringBuilder(eventUris.get(0)); + StringBuilder urisBuilder = new StringBuilder(eventUris.getFirst()); for (int i = 1; i < eventUris.size(); i++) { urisBuilder.append(",").append(eventUris.get(i)); } Map parameters = Map.of("uris", urisBuilder.toString(), "unique", unique); - List dtos = getStatsList(PREFIX_STATS, parameters); - return dtos; + return getStatsList(PREFIX_STATS, parameters); } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java index 49fecb3..f0ffbd5 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java @@ -1,13 +1,10 @@ package ru.practicum.evmsevice.controller; -import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import ru.practicum.evmsevice.client.StatsClient; import ru.practicum.evmsevice.dto.*; import ru.practicum.evmsevice.mapper.CategoryMapper; import ru.practicum.evmsevice.mapper.UserMapper; @@ -28,23 +25,18 @@ @RestController @RequestMapping("/admin") public class AdminController { - private final StatsClient statsClient; private final UserService userService; private final CategoryService categoryService; private final EventService eventService; private final CompilationService compilationService; - @Value("${spring.application.name}") - private String appName; @GetMapping("/users") @ResponseStatus(HttpStatus.OK) public List getUsers( @RequestParam(name = "ids", required = false) List ids, @RequestParam(name = "from", defaultValue = "0") Integer from, - @RequestParam(name = "size", defaultValue = "10") Integer size, - HttpServletRequest request) { + @RequestParam(name = "size", defaultValue = "10") Integer size) { log.info("Администратор запрашивает запрашивает список пользователей. {}", ids); - statsClient.hitInfo(appName, request); List users; if (ids != null) { users = userService.getUsers(ids); @@ -60,36 +52,31 @@ public List getUsers( @GetMapping("/users/{id}") @ResponseStatus(HttpStatus.OK) - public UserDto getUser(HttpServletRequest request, @PathVariable Integer id) { + public UserDto getUser(@PathVariable Integer id) { log.info("Выполняем поиск пользователя id={}.", id); - statsClient.hitInfo(appName, request); User user = userService.getUserById(id); return UserMapper.toUserDto(user); } @PostMapping("/users") @ResponseStatus(HttpStatus.CREATED) - public UserDto createUser(@Validated @RequestBody UserDto userDto, HttpServletRequest request) { + public UserDto createUser(@Validated @RequestBody UserDto userDto) { log.info("Создаем нового пользователя {}", userDto.toString()); - statsClient.hitInfo(appName, request); User savedUser = userService.addUser(UserMapper.toUser(userDto)); return UserMapper.toUserDto(savedUser); } @DeleteMapping("/users/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) - public void deleteUser(HttpServletRequest request, @PathVariable Integer id) { + public void deleteUser(@PathVariable Integer id) { log.info("Удаляем пользователя {}", id); - statsClient.hitInfo(appName, request); userService.deleteUser(id); } @PostMapping("/categories") @ResponseStatus(HttpStatus.CREATED) - public CategoryDto createCategory(@Validated @RequestBody NewCategoryDto categoryDto, - HttpServletRequest request) { + public CategoryDto createCategory(@Validated @RequestBody NewCategoryDto categoryDto) { log.info("Создаем категорию {}.", categoryDto.getName()); - statsClient.hitInfo(appName, request); Category newCategory = categoryService.createCategory(CategoryMapper.toCategory(categoryDto)); return CategoryMapper.toDto(newCategory); } @@ -97,10 +84,8 @@ public CategoryDto createCategory(@Validated @RequestBody NewCategoryDto categor @PatchMapping("/categories/{id}") @ResponseStatus(HttpStatus.OK) public CategoryDto updateCategory(@Validated @RequestBody NewCategoryDto categoryDto, - @PathVariable int id, - HttpServletRequest request) { + @PathVariable int id) { log.info("Обновляем категорию id={}.", id); - statsClient.hitInfo(appName, request); Category category = CategoryMapper.toCategory(categoryDto); category.setId(id); Category updatedCategory = categoryService.updateCategory(category); @@ -109,9 +94,8 @@ public CategoryDto updateCategory(@Validated @RequestBody NewCategoryDto categor @DeleteMapping("/categories/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) - public void deletecategory(@PathVariable int id, HttpServletRequest request) { + public void deleteCategory(@PathVariable int id) { log.info("Администратор удаляет категорию id={}.", id); - statsClient.hitInfo(appName, request); categoryService.deleteCategory(id); } @@ -124,8 +108,7 @@ public List findEvents( @RequestParam(name = "rangeStart", required = false) String rangeStart, @RequestParam(name = "rangeEnd", required = false) String rangeEnd, @RequestParam(name = "from", defaultValue = "0") Integer from, - @RequestParam(name = "size", defaultValue = "10") Integer size, - HttpServletRequest request) { + @RequestParam(name = "size", defaultValue = "10") Integer size) { log.info("Администратор запрашивает список событий. users:{}, states:{}, categories:{} rangeStart:{}, rangeStart:{}.", users, states, categories, rangeStart, rangeEnd); return eventService.findEventsByAdmin(states, users, categories, rangeStart, rangeEnd, from, size); @@ -134,7 +117,7 @@ public List findEvents( @PatchMapping("/events/{eventId}") @ResponseStatus(HttpStatus.OK) public EventFullDto updateEvent(@PathVariable Integer eventId, - @Validated @RequestBody UpdateEventAdminRequest eventDto) { + @RequestBody @Validated UpdateEventAdminRequest eventDto) { log.info("Администратор редактирует событие id={}. {}", eventId, eventDto); return eventService.adminUpdateEvent(eventId, eventDto); } @@ -142,7 +125,7 @@ public EventFullDto updateEvent(@PathVariable Integer eventId, @PostMapping("/compilations") @ResponseStatus(HttpStatus.CREATED) public CompilationDto createCompilation(@Validated @RequestBody NewCompilationDto newCompilationDto) { - log.info("Администратор создает подборку событий \'{}\'.", newCompilationDto.getTitle()); + log.info("Администратор создает подборку событий '{}'.", newCompilationDto.getTitle()); return compilationService.createCompilation(newCompilationDto); } @@ -150,7 +133,7 @@ public CompilationDto createCompilation(@Validated @RequestBody NewCompilationDt @ResponseStatus(HttpStatus.OK) public CompilationDto updateCompilation(@PathVariable Integer compId, @Validated @RequestBody PatchCompilationDto compilationDto) { - log.info("Администратор обновляет подборку событий \'{}\'.", compilationDto.getTitle()); + log.info("Администратор обновляет подборку событий '{}'.", compilationDto.getTitle()); return compilationService.patchCompilation(compId, compilationDto); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java index 579b712..872f0f3 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/ErrorAdvisor.java @@ -14,7 +14,6 @@ import java.time.LocalDateTime; import java.util.List; -import java.util.stream.Collectors; /** * Класс обработки исключений при обработке поступивших http запросов @@ -99,16 +98,16 @@ public ApiError onDataIntegrityViolationException(final DataIntegrityViolationEx public ApiError onMethodArgumentNotValidException(MethodArgumentNotValidException e) { log.error("400 {}.", e.getMessage()); final List violations = e.getBindingResult().getFieldErrors().stream() - .map(error -> String.format("Field: %s. Error: %s. Value: \'%s\'. ", + .map(error -> String.format("Field: %s. Error: %s. Value: '%s'. ", error.getField(), error.getDefaultMessage(), error.getRejectedValue() )) - .collect(Collectors.toList()); + .toList(); ApiError apiError = new ApiError(); apiError.setStatus(HttpStatus.BAD_REQUEST); apiError.setReason("Запрос сформирован некорректно."); - apiError.setMessage(violations.stream().collect(Collectors.joining())); + apiError.setMessage(String.join(" ", violations)); apiError.setTimestamp(LocalDateTime.now()); return apiError; } @@ -119,16 +118,16 @@ public ApiError onConstraintValidationException(ConstraintViolationException e) log.error("400 {}.", e.getMessage()); final List violations = e.getConstraintViolations().stream() .map( - violation -> String.format("Field: %s. Error: %s. Value: \'%s\'. ", + violation -> String.format("Field: %s. Error: %s. Value: '%s'. ", violation.getPropertyPath().toString(), violation.getMessage(), violation.getInvalidValue() )) - .collect(Collectors.toList()); + .toList(); ApiError apiError = new ApiError(); apiError.setStatus(HttpStatus.BAD_REQUEST); apiError.setReason("Запрос сформирован некорректно."); - apiError.setMessage(violations.stream().collect(Collectors.joining())); + apiError.setMessage(String.join(" ", violations)); apiError.setTimestamp(LocalDateTime.now()); return apiError; } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java index 2b373a2..dc2afec 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java @@ -26,7 +26,6 @@ @RestController @RequestMapping() public class PublicController { - private static final String RGEXP_DATE_TIME = "yyyy-MM-dd' 'HH:mm:ss"; private final StatsClient statsClient; private final EventService eventService; private final CompilationService compilationService; @@ -45,7 +44,7 @@ public EventFullDto findEventById(@PathVariable("id") int id, throw new NotFoundException("Среди опубликованных не найдено событие id=" + id); } // сохраняем запрос в сервере статистики - statsClient.hitInfo(appName, request); + statsClient.hitInfo(appName, request.getRequestURI(), request.getRemoteAddr()); return EventMapper.toFullDto(event); } @@ -56,10 +55,8 @@ public List findAllEvents( @RequestParam(name = "categories", required = false) List categories, @RequestParam(name = "paid", required = false) Boolean paid, @RequestParam(name = "rangeStart", required = false) - //@Valid @Pattern(regexp = RGEXP_DATE_TIME, message = "Не верный формат даты.") String rangeStart, @RequestParam(name = "rangeEnd", required = false) - //@Valid @Pattern(regexp = RGEXP_DATE_TIME, message = "Не верный формат даты.") String rangeEnd, @RequestParam(name = "onlyAvailable", defaultValue = "false") Boolean onlyAvailable, @RequestParam(name = "sort", defaultValue = "EVENT_DATE") String sort, @@ -70,7 +67,9 @@ public List findAllEvents( text, categories, rangeStart, rangeEnd); List eventDtos = eventService.findEventsByParametrs(text, categories, paid, rangeStart, rangeEnd, onlyAvailable, sort, from, size); - statsClient.hitInfo(appName, request); + for (EventShortDto eventDto : eventDtos) { + statsClient.hitInfo(appName, String.format("/events/%d", eventDto.getId()), request.getRemoteAddr()); + } return eventDtos; } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java index bad73df..29f8748 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java @@ -82,7 +82,7 @@ public RequestGroupDto patchRequestsByEventId(@PathVariable int userId, @PostMapping("/{userId}/requests") @ResponseStatus(HttpStatus.CREATED) public RequestDto createRequest(@PathVariable Integer userId, - @RequestParam(name = "eventId", required = true) Integer eventId) { + @RequestParam(name = "eventId") Integer eventId) { log.info("Пользователь id={} создает запрос на участие в событии id={}.", userId, eventId); Request request = requestService.createRequest(userId, eventId); diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java index 4d46ad2..5b0278d 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/EventServiceImpl.java @@ -50,7 +50,7 @@ public class EventServiceImpl implements EventService { * Создание нового события * * @param newEventDto - новое событие - * @param userId - иденитификатор пользователя инициатора + * @param userId - идентификатор пользователя инициатора * @return - сохраненный объект информации о событии */ @Override @@ -115,7 +115,7 @@ public List getEventsByUserId(Integer userId, Integer from, Integ * @param events - список событий */ private void updateViwesAndRequests(List events) { - TreeMap eventMap = new TreeMap(); + TreeMap eventMap = new TreeMap<>(); List eventUris = new ArrayList<>(); for (Event event : events) { eventMap.put(event.getId(), event); @@ -225,12 +225,14 @@ public EventFullDto adminUpdateEvent(Integer eventId, UpdateEventAdminRequest ev Event event = eventRepository.findById(eventId) .orElseThrow(() -> new NotFoundException("Не найдено событие id=" + eventId)); - if (event.getEventDate().isBefore(LocalDateTime.now().plusHours(HOURS_EVENT_DELAY))) { - throw new ValidationException( - "Field: eventDate. Error: не может быть раньше, чем через " - + HOURS_EVENT_DELAY + " часа от текущего момента. Value: " - + event.getEventDate().format(DATA_TIME_FORMATTER) - ); + if (event.getEventDate() != null) { + if (event.getEventDate().isBefore(LocalDateTime.now().plusHours(HOURS_EVENT_DELAY))) { + throw new ValidationException( + "Field: eventDate. Error: не может быть раньше, чем через " + + HOURS_EVENT_DELAY + " часа от текущего момента. Value: " + + event.getEventDate().format(DATA_TIME_FORMATTER) + ); + } } if (eventDto.getAnnotation() != null) { event.setAnnotation(eventDto.getAnnotation()); @@ -311,7 +313,7 @@ public Event findEventById(Integer eventId) { } /** - * поиск событий пользователем + * Поиск событий пользователем */ @Override public List findEventsByParametrs(String text, List categories, @@ -337,7 +339,7 @@ public List findEventsByParametrs(String text, List cate if (startDate != null && endDate != null) { if (startDate.isAfter(endDate)) { throw new BadRequestException( - "Parametr: rangeStart, rangeEnd. " + + "Parameter: rangeStart, rangeEnd. " + "Error: Введен некорректный интервал времени." + ". Value: " + startDate.format(DATA_TIME_FORMATTER) + ", " + endDate.format(DATA_TIME_FORMATTER) @@ -357,7 +359,7 @@ public List findEventsByParametrs(String text, List cate spec = spec.and(EventSpecification.annotetionContains(text)); spec = spec.or(EventSpecification.descriptionContains(text)); } - // ... поиск по списку идентификаторов категорй + // ... поиск по списку идентификаторов категорий if (categories != null) { spec = spec.and(EventSpecification.categoryIn(categories)); } @@ -381,7 +383,7 @@ public List findEventsByParametrs(String text, List cate updateViwesAndRequests(events); List eventDtos; if (onlyAvailable) { - // Фильтруем события у котрых не исчерпано количество заявок + // Фильтруем события у которых не исчерпано количество заявок eventDtos = events.stream() .filter(event -> event.getParticipantLimit() != 0 && event.getConfirmedRequests() < event.getParticipantLimit()) @@ -432,7 +434,7 @@ public List findEventsByAdmin(List states, if (users != null) { spec = spec.and(EventSpecification.eventInitiatorIdIn(users)); } - // ... поиск по списку идентификаторов категорй + // ... поиск по списку идентификаторов категорий if (categories != null) { spec = spec.and(EventSpecification.categoryIn(categories)); } diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatDbStorage.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatDbStorage.java index 1960875..a56a5c2 100644 --- a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatDbStorage.java +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatDbStorage.java @@ -54,39 +54,41 @@ public void addHit(EndpointHit hit) { @Override public List getViewStats(LocalDateTime start, LocalDateTime end, List uris, Boolean unique, Integer size) { StringBuilder sql = new StringBuilder(); - sql.append("SELECT app, uri, count(ip) as hits FROM"); + sql.append("SELECT e.app, e.uri, count(e.ip) as hits FROM"); if (unique) { - sql.append(" (SELECT DISTINCT ON (ip, uri) app, uri, ip, timestamp FROM endpointhits)"); + sql.append(" (SELECT DISTINCT ON (ip, uri) app, uri, ip, timestamp FROM endpointhits) AS e"); } else { - sql.append(" endpointhits"); + sql.append(" endpointhits AS e"); } MapSqlParameterSource parameters = new MapSqlParameterSource(); Boolean whereFlag = false; if (uris != null && !uris.isEmpty()) { - sql.append(" WHERE uri IN (:uris)"); - parameters.addValue("uris", uris); - whereFlag = true; + if (!uris.get(0).equalsIgnoreCase("/events")) { + sql.append(" WHERE e.uri IN (:uris)"); + parameters.addValue("uris", uris); + whereFlag = true; + } } if (start != null) { if (whereFlag) { - sql.append(" AND timestamp >= :start"); + sql.append(" AND e.timestamp >= :start"); } else { - sql.append(" WHERE timestamp >= :start"); + sql.append(" WHERE e.timestamp >= :start"); whereFlag = true; } parameters.addValue("start", start); } if (end != null) { if (whereFlag) { - sql.append(" AND timestamp < :end"); + sql.append(" AND e.timestamp < :end"); } else { - sql.append(" WHERE timestamp < :end"); + sql.append(" WHERE e.timestamp < :end"); } parameters.addValue("end", end); } - sql.append(" GROUP BY uri, app ORDER BY hits DESC"); + sql.append(" GROUP BY e.uri, e.app ORDER BY hits DESC"); if (size != null) { parameters.addValue("size", size); sql.append(" LIMIT :size"); From 803a18017f17c4d4999ac8d7244e1b738d3ec45a Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Wed, 9 Jul 2025 23:16:35 +0700 Subject: [PATCH 29/29] =?UTF-8?q?fix:=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B0=D0=BC=D0=B5?= =?UTF-8?q?=D1=87=D0=B0=D0=BD=D0=B8=D0=B9=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminCategoriesController.java | 40 +++++ .../AdminCompilationsController.java | 41 +++++ .../evmsevice/controller/AdminController.java | 146 ------------------ .../controller/AdminEventsController.java | 46 ++++++ .../controller/AdminUsersController.java | 65 ++++++++ .../controller/PublicController.java | 17 +- .../evmsevice/model/Compilation.java | 1 - .../evmsevice/repository/EventRepository.java | 2 + .../evmsevice/service/CategoryService.java | 6 +- .../service/CategoryServiceImpl.java | 33 +++- .../evmsevice/service/RequestServiceImpl.java | 1 + .../evmsevice/service/UserServiceImpl.java | 2 +- 12 files changed, 235 insertions(+), 165 deletions(-) create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminCategoriesController.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminCompilationsController.java delete mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminEventsController.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminUsersController.java diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminCategoriesController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminCategoriesController.java new file mode 100644 index 0000000..0dd1e9c --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminCategoriesController.java @@ -0,0 +1,40 @@ +package ru.practicum.evmsevice.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.evmsevice.dto.CategoryDto; +import ru.practicum.evmsevice.dto.NewCategoryDto; +import ru.practicum.evmsevice.service.CategoryService; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping("/admin/categories") +public class AdminCategoriesController { + private final CategoryService categoryService; + + @PostMapping() + @ResponseStatus(HttpStatus.CREATED) + public CategoryDto createCategory(@Validated @RequestBody NewCategoryDto categoryDto) { + log.info("Создаем категорию {}.", categoryDto.getName()); + return categoryService.createCategory(categoryDto); + } + + @PatchMapping("/{id}") + @ResponseStatus(HttpStatus.OK) + public CategoryDto updateCategory(@Validated @RequestBody NewCategoryDto categoryDto, + @PathVariable int id) { + log.info("Обновляем категорию id={}.", id); + return categoryService.updateCategory(id, categoryDto); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteCategory(@PathVariable int id) { + log.info("Администратор удаляет категорию id={}.", id); + categoryService.deleteCategory(id); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminCompilationsController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminCompilationsController.java new file mode 100644 index 0000000..7c238f8 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminCompilationsController.java @@ -0,0 +1,41 @@ +package ru.practicum.evmsevice.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.evmsevice.dto.CompilationDto; +import ru.practicum.evmsevice.dto.NewCompilationDto; +import ru.practicum.evmsevice.dto.PatchCompilationDto; +import ru.practicum.evmsevice.service.CompilationService; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping("/admin/compilations") +public class AdminCompilationsController { + private final CompilationService compilationService; + + @PostMapping() + @ResponseStatus(HttpStatus.CREATED) + public CompilationDto createCompilation(@Validated @RequestBody NewCompilationDto newCompilationDto) { + log.info("Администратор создает подборку событий '{}'.", newCompilationDto.getTitle()); + return compilationService.createCompilation(newCompilationDto); + } + + @PatchMapping("/{compId}") + @ResponseStatus(HttpStatus.OK) + public CompilationDto updateCompilation(@PathVariable Integer compId, + @Validated @RequestBody PatchCompilationDto compilationDto) { + log.info("Администратор обновляет подборку событий '{}'.", compilationDto.getTitle()); + return compilationService.patchCompilation(compId, compilationDto); + } + + @DeleteMapping("/{compId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteCompilation(@PathVariable Integer compId) { + log.info("Администратор удаляет подборку событий id={}.", compId); + compilationService.deleteCompilation(compId); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java deleted file mode 100644 index f0ffbd5..0000000 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminController.java +++ /dev/null @@ -1,146 +0,0 @@ -package ru.practicum.evmsevice.controller; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; -import ru.practicum.evmsevice.dto.*; -import ru.practicum.evmsevice.mapper.CategoryMapper; -import ru.practicum.evmsevice.mapper.UserMapper; -import ru.practicum.evmsevice.model.Category; -import ru.practicum.evmsevice.model.User; -import ru.practicum.evmsevice.service.CategoryService; -import ru.practicum.evmsevice.service.CompilationService; -import ru.practicum.evmsevice.service.EventService; -import ru.practicum.evmsevice.service.UserService; - -import java.util.List; - -/** - * Класс обработки запросов администратора - */ -@Slf4j -@RequiredArgsConstructor -@RestController -@RequestMapping("/admin") -public class AdminController { - private final UserService userService; - private final CategoryService categoryService; - private final EventService eventService; - private final CompilationService compilationService; - - @GetMapping("/users") - @ResponseStatus(HttpStatus.OK) - public List getUsers( - @RequestParam(name = "ids", required = false) List ids, - @RequestParam(name = "from", defaultValue = "0") Integer from, - @RequestParam(name = "size", defaultValue = "10") Integer size) { - log.info("Администратор запрашивает запрашивает список пользователей. {}", ids); - List users; - if (ids != null) { - users = userService.getUsers(ids); - } else { - users = userService.getUsers(); - } - return users.stream() - .map(UserMapper::toUserDto) - .skip(from) - .limit(size) - .toList(); - } - - @GetMapping("/users/{id}") - @ResponseStatus(HttpStatus.OK) - public UserDto getUser(@PathVariable Integer id) { - log.info("Выполняем поиск пользователя id={}.", id); - User user = userService.getUserById(id); - return UserMapper.toUserDto(user); - } - - @PostMapping("/users") - @ResponseStatus(HttpStatus.CREATED) - public UserDto createUser(@Validated @RequestBody UserDto userDto) { - log.info("Создаем нового пользователя {}", userDto.toString()); - User savedUser = userService.addUser(UserMapper.toUser(userDto)); - return UserMapper.toUserDto(savedUser); - } - - @DeleteMapping("/users/{id}") - @ResponseStatus(HttpStatus.NO_CONTENT) - public void deleteUser(@PathVariable Integer id) { - log.info("Удаляем пользователя {}", id); - userService.deleteUser(id); - } - - @PostMapping("/categories") - @ResponseStatus(HttpStatus.CREATED) - public CategoryDto createCategory(@Validated @RequestBody NewCategoryDto categoryDto) { - log.info("Создаем категорию {}.", categoryDto.getName()); - Category newCategory = categoryService.createCategory(CategoryMapper.toCategory(categoryDto)); - return CategoryMapper.toDto(newCategory); - } - - @PatchMapping("/categories/{id}") - @ResponseStatus(HttpStatus.OK) - public CategoryDto updateCategory(@Validated @RequestBody NewCategoryDto categoryDto, - @PathVariable int id) { - log.info("Обновляем категорию id={}.", id); - Category category = CategoryMapper.toCategory(categoryDto); - category.setId(id); - Category updatedCategory = categoryService.updateCategory(category); - return CategoryMapper.toDto(updatedCategory); - } - - @DeleteMapping("/categories/{id}") - @ResponseStatus(HttpStatus.NO_CONTENT) - public void deleteCategory(@PathVariable int id) { - log.info("Администратор удаляет категорию id={}.", id); - categoryService.deleteCategory(id); - } - - @GetMapping("/events") - @ResponseStatus(HttpStatus.OK) - public List findEvents( - @RequestParam(name = "users", required = false) List users, - @RequestParam(name = "states", required = false) List states, - @RequestParam(name = "categories", required = false) List categories, - @RequestParam(name = "rangeStart", required = false) String rangeStart, - @RequestParam(name = "rangeEnd", required = false) String rangeEnd, - @RequestParam(name = "from", defaultValue = "0") Integer from, - @RequestParam(name = "size", defaultValue = "10") Integer size) { - log.info("Администратор запрашивает список событий. users:{}, states:{}, categories:{} rangeStart:{}, rangeStart:{}.", - users, states, categories, rangeStart, rangeEnd); - return eventService.findEventsByAdmin(states, users, categories, rangeStart, rangeEnd, from, size); - } - - @PatchMapping("/events/{eventId}") - @ResponseStatus(HttpStatus.OK) - public EventFullDto updateEvent(@PathVariable Integer eventId, - @RequestBody @Validated UpdateEventAdminRequest eventDto) { - log.info("Администратор редактирует событие id={}. {}", eventId, eventDto); - return eventService.adminUpdateEvent(eventId, eventDto); - } - - @PostMapping("/compilations") - @ResponseStatus(HttpStatus.CREATED) - public CompilationDto createCompilation(@Validated @RequestBody NewCompilationDto newCompilationDto) { - log.info("Администратор создает подборку событий '{}'.", newCompilationDto.getTitle()); - return compilationService.createCompilation(newCompilationDto); - } - - @PatchMapping("/compilations/{compId}") - @ResponseStatus(HttpStatus.OK) - public CompilationDto updateCompilation(@PathVariable Integer compId, - @Validated @RequestBody PatchCompilationDto compilationDto) { - log.info("Администратор обновляет подборку событий '{}'.", compilationDto.getTitle()); - return compilationService.patchCompilation(compId, compilationDto); - } - - @DeleteMapping("/compilations/{compId}") - @ResponseStatus(HttpStatus.NO_CONTENT) - public void deleteCompilation(@PathVariable Integer compId) { - log.info("Администратор удаляет подборку событий id={}.", compId); - compilationService.deleteCompilation(compId); - } -} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminEventsController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminEventsController.java new file mode 100644 index 0000000..2c348d6 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminEventsController.java @@ -0,0 +1,46 @@ +package ru.practicum.evmsevice.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.evmsevice.dto.EventFullDto; +import ru.practicum.evmsevice.dto.UpdateEventAdminRequest; +import ru.practicum.evmsevice.service.EventService; + +import java.util.List; + +/** + * Класс обработки запросов администратора + */ +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping("/admin/events") +public class AdminEventsController { + private final EventService eventService; + + @GetMapping() + @ResponseStatus(HttpStatus.OK) + public List findEvents( + @RequestParam(name = "users", required = false) List users, + @RequestParam(name = "states", required = false) List states, + @RequestParam(name = "categories", required = false) List categories, + @RequestParam(name = "rangeStart", required = false) String rangeStart, + @RequestParam(name = "rangeEnd", required = false) String rangeEnd, + @RequestParam(name = "from", defaultValue = "0") Integer from, + @RequestParam(name = "size", defaultValue = "10") Integer size) { + log.info("Администратор запрашивает список событий. users:{}, states:{}, categories:{} rangeStart:{}, rangeStart:{}.", + users, states, categories, rangeStart, rangeEnd); + return eventService.findEventsByAdmin(states, users, categories, rangeStart, rangeEnd, from, size); + } + + @PatchMapping("/{eventId}") + @ResponseStatus(HttpStatus.OK) + public EventFullDto updateEvent(@PathVariable Integer eventId, + @RequestBody @Validated UpdateEventAdminRequest eventDto) { + log.info("Администратор редактирует событие id={}. {}", eventId, eventDto); + return eventService.adminUpdateEvent(eventId, eventDto); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminUsersController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminUsersController.java new file mode 100644 index 0000000..b966249 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/AdminUsersController.java @@ -0,0 +1,65 @@ +package ru.practicum.evmsevice.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.evmsevice.dto.UserDto; +import ru.practicum.evmsevice.mapper.UserMapper; +import ru.practicum.evmsevice.model.User; +import ru.practicum.evmsevice.service.UserService; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping("/admin/users") +public class AdminUsersController { + private final UserService userService; + + @GetMapping() + @ResponseStatus(HttpStatus.OK) + public List getUsers( + @RequestParam(name = "ids", required = false) List ids, + @RequestParam(name = "from", defaultValue = "0") Integer from, + @RequestParam(name = "size", defaultValue = "10") Integer size) { + log.info("Администратор запрашивает запрашивает список пользователей. {}", ids); + List users; + if (ids != null) { + users = userService.getUsers(ids); + } else { + users = userService.getUsers(); + } + return users.stream() + .map(UserMapper::toUserDto) + .skip(from) + .limit(size) + .toList(); + } + + @GetMapping("/{id}") + @ResponseStatus(HttpStatus.OK) + public UserDto getUser(@PathVariable Integer id) { + log.info("Выполняем поиск пользователя id={}.", id); + User user = userService.getUserById(id); + return UserMapper.toUserDto(user); + } + + @PostMapping() + @ResponseStatus(HttpStatus.CREATED) + public UserDto createUser(@Validated @RequestBody UserDto userDto) { + log.info("Создаем нового пользователя {}", userDto.toString()); + User savedUser = userService.addUser(UserMapper.toUser(userDto)); + return UserMapper.toUserDto(savedUser); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteUser(@PathVariable Integer id) { + log.info("Удаляем пользователя {}", id); + userService.deleteUser(id); + } + +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java index dc2afec..f2d57af 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java @@ -7,13 +7,14 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import ru.practicum.evmsevice.client.StatsClient; +import ru.practicum.evmsevice.dto.CategoryDto; import ru.practicum.evmsevice.dto.CompilationDto; import ru.practicum.evmsevice.dto.EventFullDto; import ru.practicum.evmsevice.dto.EventShortDto; import ru.practicum.evmsevice.enums.EventState; import ru.practicum.evmsevice.exception.NotFoundException; +import ru.practicum.evmsevice.mapper.CategoryMapper; import ru.practicum.evmsevice.mapper.EventMapper; -import ru.practicum.evmsevice.model.Category; import ru.practicum.evmsevice.model.Event; import ru.practicum.evmsevice.service.CategoryService; import ru.practicum.evmsevice.service.CompilationService; @@ -92,17 +93,19 @@ public CompilationDto findCompilationById(@PathVariable("compId") int compId) { @GetMapping("/categories") @ResponseStatus(HttpStatus.OK) - public List findCategories(@RequestParam(name = "from", defaultValue = "0") Integer from, - @RequestParam(name = "size", defaultValue = "10") Integer size) { + public List findCategories(@RequestParam(name = "from", defaultValue = "0") Integer from, + @RequestParam(name = "size", defaultValue = "10") Integer size) { log.info("Пользователь запрашивает список категорий."); - return categoryService.getAllCategories().stream().skip(from).limit(size).toList(); + return categoryService.getAllCategories().stream() + .map(CategoryMapper::toDto) + .skip(from).limit(size) + .toList(); } @GetMapping("/categories/{catId}") @ResponseStatus(HttpStatus.OK) - public Category findCategoryById(@PathVariable("catId") int catId) { + public CategoryDto findCategoryById(@PathVariable("catId") int catId) { log.info("Пользователь запрашивает категорию id={}.", catId); - return categoryService.getCategoryById(catId); + return CategoryMapper.toDto(categoryService.getCategoryById(catId)); } - } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Compilation.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Compilation.java index 4369bd2..1c64f63 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Compilation.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Compilation.java @@ -22,7 +22,6 @@ public class Compilation { joinColumns = {@JoinColumn(name = "compilation_id")}, inverseJoinColumns = {@JoinColumn(name = "event_id")}) private Set events = new HashSet<>(); - //private List events = new ArrayList<>(); @Column(name = "title", nullable = false) private String title; @Column(name = "pinned", nullable = false) diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java index be0b000..5a3f866 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/EventRepository.java @@ -11,4 +11,6 @@ public interface EventRepository extends JpaRepository, List findEventsByInitiator_Id(int id); List findEventsByIdIn(List ids); + + List findEventsByCategory_Id(int id); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryService.java index 9cc6049..c2cc218 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryService.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryService.java @@ -1,13 +1,15 @@ package ru.practicum.evmsevice.service; +import ru.practicum.evmsevice.dto.CategoryDto; +import ru.practicum.evmsevice.dto.NewCategoryDto; import ru.practicum.evmsevice.model.Category; import java.util.List; public interface CategoryService { - Category createCategory(Category category); + CategoryDto createCategory(NewCategoryDto categoryDto); - Category updateCategory(Category category); + CategoryDto updateCategory(Integer id, NewCategoryDto categoryDto); void deleteCategory(Integer id); diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryServiceImpl.java index 7ed6c84..2de8c9b 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CategoryServiceImpl.java @@ -3,40 +3,57 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import ru.practicum.evmsevice.dto.CategoryDto; +import ru.practicum.evmsevice.dto.NewCategoryDto; +import ru.practicum.evmsevice.exception.DataConflictException; import ru.practicum.evmsevice.exception.NotFoundException; +import ru.practicum.evmsevice.mapper.CategoryMapper; import ru.practicum.evmsevice.model.Category; +import ru.practicum.evmsevice.model.Event; import ru.practicum.evmsevice.repository.CategoryRepository; +import ru.practicum.evmsevice.repository.EventRepository; import java.util.List; @Service -@Transactional +@Transactional(readOnly = true) @RequiredArgsConstructor public class CategoryServiceImpl implements CategoryService { private final CategoryRepository categoryRepository; + private final EventRepository eventRepository; @Override - public Category createCategory(Category category) { - Category savedCategory = categoryRepository.save(category); - return savedCategory; + @Transactional + public CategoryDto createCategory(NewCategoryDto categoryDto) { + Category savedCategory = categoryRepository.save(CategoryMapper.toCategory(categoryDto)); + return CategoryMapper.toDto(savedCategory); } @Override - public Category updateCategory(Category category) { - categoryRepository.findById(category.getId()) - .orElseThrow(() -> new NotFoundException("Не найдена категория id=" + category.getId())); + @Transactional + public CategoryDto updateCategory(Integer id, NewCategoryDto categoryDto) { + Category category = categoryRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Не найдена категория id=" + id)); + category.setName(categoryDto.getName()); Category updatedCategory = categoryRepository.save(category); - return updatedCategory; + return CategoryMapper.toDto(updatedCategory); } @Override + @Transactional public void deleteCategory(Integer id) { categoryRepository.findById(id) .orElseThrow(() -> new NotFoundException("Не найдена категория id=" + id)); + List events = eventRepository.findEventsByCategory_Id(id); + if (events.size() > 0) { + throw new DataConflictException( + "Категория id=" + id + " не пустая."); + } categoryRepository.deleteById(id); } @Override + public Category getCategoryById(Integer id) { Category category = categoryRepository.findById(id) .orElseThrow(() -> new NotFoundException("Не найдена категория id=" + id)); diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java index 776176e..e3b69a8 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/RequestServiceImpl.java @@ -69,6 +69,7 @@ public Request createRequest(Integer userId, Integer eventId) { } @Override + @Transactional(readOnly = true) public List getRequestsByUserId(Integer userId) { List requests = requestRepository.findAllByRequester_Id(userId); if (requests.isEmpty()) { diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/UserServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/UserServiceImpl.java index 917f20a..cce780f 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/UserServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/UserServiceImpl.java @@ -43,7 +43,7 @@ public User getUserById(Integer id) { @Override public void deleteUser(Integer id) { - User user = userRepository.findById(id) + userRepository.findById(id) .orElseThrow(() -> new NotFoundException("Не найден пользователь id=" + id)); userRepository.deleteById(id);