From 9cd4791cd7749ff32b3ac8611f3d76b4e5e7ba85 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Sat, 21 Dec 2024 21:24:47 +0700 Subject: [PATCH 01/11] =?UTF-8?q?=D0=A1=D0=BF=D1=80=D0=B8=D0=BD=D1=82=2010?= =?UTF-8?q?.=20=D0=9D=D0=B0=D1=87=D0=B0=D0=BB=D0=BE.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d38b6d..b6cea1a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ # java-filmorate Учебный проект. -Созданиие прилощений на основе шаблона "Spring" \ No newline at end of file +Созданиие прилощений на основе шаблона "Spring"

+ +## Спринт 10 +Добавляем контроллеры фильмов и пользователей. \ No newline at end of file From 6adb310fc62529a48d9a262648e08765321df1c7 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Sat, 21 Dec 2024 22:39:57 +0700 Subject: [PATCH 02/11] =?UTF-8?q?=D0=A1=D0=BF=D1=80=D0=B8=D0=BD=D1=82=2010?= =?UTF-8?q?.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=BE=D0=BB=D0=BB=D0=B5=D1=80?= =?UTF-8?q?=D1=8B.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 12 + .../filmorate/FilmorateApplication.java | 12 +- .../controller/AbstractController.java | 101 +++++++ .../filmorate/controller/ErrorHandler.java | 101 +++++++ .../filmorate/controller/FilmController.java | 84 +++++- .../filmorate/controller/UserController.java | 93 +++++++ .../filmorate/controller/package-info.java | 4 + .../exception/ValidationException.java | 19 ++ .../filmorate/model/ErrorMessage.java | 13 + .../practicum/filmorate/model/Film.java | 50 +++- .../filmorate/model/LegalFilmDate.java | 21 ++ .../model/LegalFilmDateValidator.java | 26 ++ .../filmorate/model/LocalDateAdapter.java | 35 +++ .../practicum/filmorate/model/Marker.java | 16 ++ .../filmorate/model/StorageData.java | 13 + .../practicum/filmorate/model/User.java | 48 ++++ .../model/ValidationErrorResponse.java | 16 ++ .../practicum/filmorate/model/Violation.java | 14 + .../filmorate/model/package-info.java | 4 + .../practicum/filmorate/package-info.java | 4 + .../controller/AbstractControllerTest.java | 251 ++++++++++++++++++ .../controller/FilmControllerTest.java | 125 +++++++++ .../controller/UserControllerTest.java | 127 +++++++++ .../practicum/filmorate/model/FilmTest.java | 134 ++++++++++ .../practicum/filmorate/model/UserTest.java | 208 +++++++++++++++ 25 files changed, 1521 insertions(+), 10 deletions(-) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/controller/AbstractController.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/controller/package-info.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/exception/ValidationException.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/ErrorMessage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/LegalFilmDate.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/LegalFilmDateValidator.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/LocalDateAdapter.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/Marker.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/StorageData.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/User.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/ValidationErrorResponse.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/Violation.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/package-info.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/package-info.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/controller/AbstractControllerTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java diff --git a/pom.xml b/pom.xml index 0cad031..596a9fc 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,11 @@ 21 + + com.google.code.gson + gson + + org.springframework.boot spring-boot-starter-web @@ -27,11 +32,18 @@ lombok provided + org.springframework.boot spring-boot-starter-test test + + + org.springframework.boot + spring-boot-starter-validation + + diff --git a/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java b/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java index dca451b..643c0c2 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java +++ b/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java @@ -3,10 +3,18 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** + * Главный класс приложения рейтинга фильмов. + */ @SpringBootApplication public class FilmorateApplication { - public static void main(String[] args) { + + /** + * Запуск приложения. + * + * @param args - параметры запуска. + */ + public static void main(final String[] args) { SpringApplication.run(FilmorateApplication.class, args); } - } diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/AbstractController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/AbstractController.java new file mode 100644 index 0000000..3306c55 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/AbstractController.java @@ -0,0 +1,101 @@ +package ru.yandex.practicum.filmorate.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestBody; +import ru.yandex.practicum.filmorate.exception.ValidationException; +import ru.yandex.practicum.filmorate.model.Marker; +import ru.yandex.practicum.filmorate.model.StorageData; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * Класс работы с элементами хранилища информации + * + * @param + */ +public class AbstractController { + private final Map storage = new HashMap<>(); + + /** + * Метод чтения элемента из хранилища + * + * @param id - идентификатор элемента + * @return - объект + */ + public T getElement(final Integer id) { + T element = storage.get(id); + if (element == null) { + throw new ValidationException(HttpStatus.NOT_FOUND, + "Не найден Id=" + id); + } + return storage.get(id); + } + + /** + * Метод поиска всех элементов + * + * @return - список элементов + */ + public Collection findAll() { + return storage.values(); + } + + /** + * Метод добавления нового элемента + * + * @param element - объект для добавления + * @return - подтверждение добавленного объекта + */ + public T addNew(@Validated @RequestBody T element) throws ValidationException { + // Проверяем существование полльзователя для исключения дублирования + if (storage.containsValue(element)) { + throw new ValidationException(HttpStatus.BAD_REQUEST, "Уже существует : " + + element.toString()); + } + + element.setId(getNextId()); + storage.put(element.getId(), element); + return element; + } + + /** + * Метод обновления элемента. + * + * @param element - объект с обновленной информацией + * @return - подтверждение обновленного объекта + */ + public T update(@Validated(Marker.OnUpdate.class) @RequestBody T element) throws ValidationException { + Integer id = element.getId(); + // проверяем необходимые условия + if (!storage.containsKey(id)) { + throw new ValidationException(HttpStatus.NOT_FOUND, + "Не найден Id=" + id); + } + storage.put(id, element); + return element; + } + + /** + * Вспомогательный метод для генерации идентификатора нового элемента + * + * @return - актуальный идентификатор + */ + private Integer getNextId() { + Integer currentMaxId = storage.keySet() + .stream() + .mapToInt(id -> id) + .max() + .orElse(0); + return ++currentMaxId; + } + + /** + * Метод очистки хранилища + */ + public void clear() { + storage.clear(); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java b/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java new file mode 100644 index 0000000..e65b2fc --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java @@ -0,0 +1,101 @@ +package ru.yandex.practicum.filmorate.controller; + +import jakarta.validation.ConstraintViolationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import ru.yandex.practicum.filmorate.exception.ValidationException; +import ru.yandex.practicum.filmorate.model.ErrorMessage; +import ru.yandex.practicum.filmorate.model.ValidationErrorResponse; +import ru.yandex.practicum.filmorate.model.Violation; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Класс обработки исключений при обработке поступивших http запросов + */ +@ControllerAdvice +public class ErrorHandler { + private static final Logger log = LoggerFactory.getLogger(ErrorHandler.class); + + /** + * Обработка исключения ConstraintViolationException - при проверке ограничений объекта + * + * @param e - исключение + * @return - список нарушений для отображения в теле ответа + */ + @ResponseBody + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ValidationErrorResponse onConstraintValidationException(ConstraintViolationException e) { + final List violations = e.getConstraintViolations().stream() + .map( + violation -> new Violation( + violation.getPropertyPath().toString(), + violation.getMessage() + ) + ) + .collect(Collectors.toList()); + + log.info("404 {}.", e.getMessage()); + return new ValidationErrorResponse(violations); + } + + /** + * Обработка исключения MethodArgumentNotValidException - при проверке аргумента метода + * + * @param e - исключение + * @return - список нарушений для отображения в теле ответа + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseBody + public ValidationErrorResponse onMethodArgumentNotValidException( + MethodArgumentNotValidException e + ) { + final List violations = e.getBindingResult().getFieldErrors().stream() + .map(error -> new Violation(error.getField(), error.getDefaultMessage())) + .collect(Collectors.toList()); + log.info("404 {}.", e.getMessage()); + return new ValidationErrorResponse(violations); + } + + /** + * Метод обработки пользовательского исключения ValidationException + * + * @param exception - исключкние проверки данных + * @return - объект для http ответа с сообщением об ошибке + */ + @ExceptionHandler(ValidationException.class) + public ResponseEntity onValidationException(ValidationException exception) { + log.info("{} {}.", exception.getHttpStatus(), exception.getMessage()); + return ResponseEntity + .status(exception.getHttpStatus()) + .body(new ErrorMessage(exception.getMessage())); + } + + /** + * Обработка исключения HttpMessageNotReadableException при поступлении пустого запроса + * + * @param e - исключкние генерируемое при отсутствии обязательных данных в теле запроса + * @return - объект для http ответа с сообщением об ошибке + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity onHttpMessageNotReadableException( + HttpMessageNotReadableException e) { + log.info("400 {}.", e.getMessage()); + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(new ErrorMessage("В запросе отсутствуют необходимые данные.")); + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java index 08cf0a1..501097a 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -1,7 +1,87 @@ package ru.yandex.practicum.filmorate.controller; -import org.springframework.web.bind.annotation.RestController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Marker; +import java.util.Collection; + +/** + * Класс обработки http запросов к фильмам. + */ @RestController -public class FilmController { +@RequestMapping("/films") +public class FilmController extends AbstractController { + private static final Logger log = LoggerFactory.getLogger(FilmController.class); + + /** + * Метод поиска всех фильмов + * + * @return - список фильмов + */ + @GetMapping + public Collection findAllFilms() { + log.info("Get all films {}.", super.findAll().size()); + return super.findAll(); + } + + /** + * Метод добавления нового фильма. + * + * @param film - объект для добавления + * @return - подтверждение добавленного объекта + */ + @PostMapping + public Film addNewFilm(@Validated(Marker.OnBasic.class) @RequestBody Film film) { + log.info("Creating film: {}.", film.toString()); + return super.addNew(film); + } + + + /** + * Метод обновления информации о фильме. + * При вызове метода промзводится проверка аннотаций только для маркера OnUpdate.class. + * Кроме id любой другой параметр может отсутствовать + * + * @param updFilm - объект с обновленной информацией о фильме + * @return - подтверждение обновленного объекта + */ + @PutMapping + public Film updateFilm(@Validated(Marker.OnUpdate.class) @RequestBody Film updFilm) { + Integer id = updFilm.getId(); + Film film = new Film(getElement(id)); + + // Обновляем информаию во временном объекте + if (updFilm.getName() != null) { + film.setName(updFilm.getName()); + } + if (updFilm.getDescription() != null) { + film.setDescription(updFilm.getDescription()); + } + if (updFilm.getReleaseDate() != null) { + film.setReleaseDate(updFilm.getReleaseDate()); + } + if (updFilm.getDuration() > 0) { + film.setDuration(updFilm.getDuration()); + } + + log.info("Updating film id={} : {}", id, film.toString()); + return super.update(film); + } + + /** + * Удаление всех фильмов + * + * @return - сообщение о выполнении + */ + @DeleteMapping + public String onDelete() { + log.info("Deleting all films."); + clear(); + return "All films deleted."; + } + } diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java new file mode 100644 index 0000000..36aaa72 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -0,0 +1,93 @@ +package ru.yandex.practicum.filmorate.controller; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.model.Marker; +import ru.yandex.practicum.filmorate.model.User; + +import java.util.Collection; + +/** + * Класс обработки http запросов о пользователях. + */ +@RestController +@RequestMapping("/users") +@Validated +public class UserController extends AbstractController { + private static final Logger log = LoggerFactory.getLogger(UserController.class); + + /** + * Метод поиска всех пользователей + * + * @return - список пользователей + */ + @GetMapping + public Collection findAllUser() { + log.info("Get all users {}.", super.findAll().size()); + return super.findAll(); + } + + /** + * Метод добавления нового пользователя. + * + * @param user - объект для добавления + * @return - подтверждение добавленного объекта + */ + @PostMapping + public User addNewUser(@Validated(Marker.OnBasic.class) @RequestBody User user) { + // "имя для отображения может быть пустым + // — в таком случае будет использован логин" (ТЗ-№10) + if (user.getName() == null || user.getName().isBlank()) { + user.setName(user.getLogin()); + } + + log.info("Creating user : {}.", user.toString()); + return super.addNew(user); + } + + /** + * Метод обновления информации о пользователе. + * При вызове метода промзводится проверка аннотаций только для маркера OnUpdate.class. + * Кроме id любой другой параметр может отсутствовать + * + * @param updUser - объект с обновленной информацией о пользователе + * @return - подтверждение обновленного объекта + */ + @PutMapping + public User updateUser(@Validated(Marker.OnUpdate.class) @RequestBody User updUser) { + Integer id = updUser.getId(); + User user = new User(super.getElement(id)); + + // Обновляем информаию во временном объекте + if (updUser.getEmail() != null) { + user.setEmail(updUser.getEmail()); + } + if (updUser.getLogin() != null) { + user.setLogin(updUser.getLogin()); + } + if (updUser.getName() != null) { + user.setName(updUser.getName()); + } + if (updUser.getBirthday() != null) { + user.setBirthday(updUser.getBirthday()); + } + + log.info("Updating user id={} : {}", id, user.toString()); + return super.update(user); + } + + /** + * Удаление всех пользователей + * + * @return - сообщение о выполнении + */ + @DeleteMapping + public String onDelete() { + log.info("Deleting all users."); + clear(); + return "All users deleted."; + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/package-info.java b/src/main/java/ru/yandex/practicum/filmorate/controller/package-info.java new file mode 100644 index 0000000..118c79f --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/package-info.java @@ -0,0 +1,4 @@ +/** + * Классоы контроллеров API приложения. + */ +package ru.yandex.practicum.filmorate.controller; diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/ValidationException.java b/src/main/java/ru/yandex/practicum/filmorate/exception/ValidationException.java new file mode 100644 index 0000000..ff42ec6 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ValidationException.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.exception; + +import org.springframework.http.HttpStatus; + +/** + * класс исключений прии проверки допустимых значений переменнх + */ +public class ValidationException extends RuntimeException { + HttpStatus httpStatus; + + public ValidationException(HttpStatus httpStatus, String message) { + super(message); + this.httpStatus = httpStatus; + } + + public HttpStatus getHttpStatus() { + return httpStatus; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/ErrorMessage.java b/src/main/java/ru/yandex/practicum/filmorate/model/ErrorMessage.java new file mode 100644 index 0000000..6d1c0e6 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/ErrorMessage.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Класс сообщения об ошибке выполнения запроса + */ +@Getter +@AllArgsConstructor +public class ErrorMessage { + private String error; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java index 3614a44..41f650e 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -1,12 +1,50 @@ package ru.yandex.practicum.filmorate.model; -import lombok.Getter; -import lombok.Setter; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDate; /** - * Film. + * Класс описания фильма. */ -@Getter -@Setter -public class Film { +@Data +@ToString(callSuper = false) +@EqualsAndHashCode(exclude = {"id", "description"}) // при сравнении не учитывать: id, description +@AllArgsConstructor +@Validated +public class Film extends StorageData { + @NotBlank(message = "Название фильма не может быть пустым.", + groups = {Marker.OnBasic.class, Marker.OnUpdate.class}) + private String name; + + @Size(min = 0, max = 200, message = "Максимальная длина описания - 200 символов.", + groups = {Marker.OnBasic.class, Marker.OnUpdate.class}) + private String description; + + @LegalFilmDate(groups = {Marker.OnBasic.class, Marker.OnUpdate.class}) + private LocalDate releaseDate; + + @Positive(message = "Длительность фильма должна быть положительным числом", + groups = {Marker.OnBasic.class, Marker.OnUpdate.class}) + private int duration; + + /** + * Конструктор копирования сведений о фильме + * + * @param original - объект копирования + */ + public Film(Film original) { + this.id = original.getId(); + this.name = original.getName(); + this.description = original.getDescription(); + this.releaseDate = original.getReleaseDate(); + this.duration = original.getDuration(); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/LegalFilmDate.java b/src/main/java/ru/yandex/practicum/filmorate/model/LegalFilmDate.java new file mode 100644 index 0000000..6a48a1c --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/LegalFilmDate.java @@ -0,0 +1,21 @@ +package ru.yandex.practicum.filmorate.model; + +import jakarta.validation.Constraint; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Интерфейс аннотации проверки даты выпуска фильма + */ +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = LegalFilmDateValidator.class) +public @interface LegalFilmDate { + String message() default "Дата выпуска фильма не должна быть ранее {value} и позднее текущей."; + + Class[] groups() default {}; + + Class[] payload() default {}; + + String value() default "1895-12-28"; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/LegalFilmDateValidator.java b/src/main/java/ru/yandex/practicum/filmorate/model/LegalFilmDateValidator.java new file mode 100644 index 0000000..23eae05 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/LegalFilmDateValidator.java @@ -0,0 +1,26 @@ +package ru.yandex.practicum.filmorate.model; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.time.LocalDate; + +/** + * Проверка аннотированного поля на допустимые значения даты + */ +public class LegalFilmDateValidator implements ConstraintValidator { + private LocalDate minimumDate; + private LocalDate maximumDate; + + @Override + public void initialize(LegalFilmDate constraintAnnotation) { + minimumDate = LocalDate.parse(constraintAnnotation.value()); + maximumDate = LocalDate.now(); + } + + @Override + public boolean isValid(LocalDate value, ConstraintValidatorContext context) { + return value == null || + (value.isAfter(minimumDate) && !value.isAfter(maximumDate)); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/LocalDateAdapter.java b/src/main/java/ru/yandex/practicum/filmorate/model/LocalDateAdapter.java new file mode 100644 index 0000000..3722b3c --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/LocalDateAdapter.java @@ -0,0 +1,35 @@ +package ru.yandex.practicum.filmorate.model; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +/** + * Класс определения правил преобразования даты + */ +public class LocalDateAdapter extends TypeAdapter { + private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + @Override + public void write(final JsonWriter jsonWriter, final LocalDate localDate) throws IOException { + if (localDate != null) { + jsonWriter.value(localDate.format(dtf)); + } else { + jsonWriter.value("null"); + } + } + + @Override + public LocalDate read(final JsonReader jsonReader) throws IOException { + String value = jsonReader.nextString(); + if (!value.equals("null")) { + return LocalDate.parse(value, dtf); + } else { + return null; + } + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Marker.java b/src/main/java/ru/yandex/practicum/filmorate/model/Marker.java new file mode 100644 index 0000000..e595677 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Marker.java @@ -0,0 +1,16 @@ +package ru.yandex.practicum.filmorate.model; + +/** + * Описание интерфейсов групп проверки аннотаций + */ +public interface Marker { + + // Основная группа проверки аннотаций + interface OnBasic { + } + + // Группа для проверки при обновлении объектов + interface OnUpdate { + } + +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/StorageData.java b/src/main/java/ru/yandex/practicum/filmorate/model/StorageData.java new file mode 100644 index 0000000..33bf6de --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/StorageData.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.model; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * Класс данных для наследования классов модел + */ +@Data +public class StorageData { + @NotNull(groups = {Marker.OnUpdate.class}, message = "id должен быть определен") + Integer id; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/User.java b/src/main/java/ru/yandex/practicum/filmorate/model/User.java new file mode 100644 index 0000000..04f123f --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java @@ -0,0 +1,48 @@ +package ru.yandex.practicum.filmorate.model; + +import jakarta.validation.constraints.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDate; + +/** + * Класс описания пользователя. + */ +@Data +@ToString(callSuper = false) +@EqualsAndHashCode(exclude = {"id", "name", "birthday"}) +@AllArgsConstructor +@Validated +public class User extends StorageData { + @NotBlank(message = "Email не может быть пустым", groups = Marker.OnBasic.class) + @Email(message = "Email должен удовлетворять правилам формирования почтовых адресов.", + groups = {Marker.OnBasic.class, Marker.OnUpdate.class}) + private String email; + + @NotBlank(message = "login не может быть пустым", groups = Marker.OnBasic.class) + @Pattern(regexp = "^[a-zA-Z0-9]{6,12}$", message = "login должен иметь длину от 6 до 12 символов, содержать буквы и цифры.", + groups = {Marker.OnBasic.class, Marker.OnUpdate.class}) + private String login; + + private String name; + + @PastOrPresent(message = "Дата рождения не может быть в будущем.", groups = {Marker.OnBasic.class, Marker.OnUpdate.class}) + private LocalDate birthday; + + /** + * Конструктор копирования сведений о пользователе + * + * @param original - объект копирования + */ + public User(User original) { + this.id = original.getId(); + this.email = original.getEmail(); + this.login = original.getLogin(); + this.name = original.getName(); + this.birthday = original.getBirthday(); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/ValidationErrorResponse.java b/src/main/java/ru/yandex/practicum/filmorate/model/ValidationErrorResponse.java new file mode 100644 index 0000000..08d0619 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/ValidationErrorResponse.java @@ -0,0 +1,16 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +/** + * Класс для формирования ответа об обнаруженных нарушениях при проверке ограничений на данные + */ +@Getter +@RequiredArgsConstructor +public class ValidationErrorResponse { + // список обнаруженных нарушений + private final List violations; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Violation.java b/src/main/java/ru/yandex/practicum/filmorate/model/Violation.java new file mode 100644 index 0000000..e28b642 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Violation.java @@ -0,0 +1,14 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Класс описания нарушений при проверке ограничений. + */ +@Getter +@RequiredArgsConstructor +public class Violation { + private final String fieldName; // Наименование поля объекта + private final String message; // Описание нарушения +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/package-info.java b/src/main/java/ru/yandex/practicum/filmorate/model/package-info.java new file mode 100644 index 0000000..72bc8b8 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/package-info.java @@ -0,0 +1,4 @@ +/** + * Описание объектов модели приложения. + */ +package ru.yandex.practicum.filmorate.model; diff --git a/src/main/java/ru/yandex/practicum/filmorate/package-info.java b/src/main/java/ru/yandex/practicum/filmorate/package-info.java new file mode 100644 index 0000000..9d4d191 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/package-info.java @@ -0,0 +1,4 @@ +/** + * Приложения расчета рейтинга просмотренных фильмов. + */ +package ru.yandex.practicum.filmorate; diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/AbstractControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/AbstractControllerTest.java new file mode 100644 index 0000000..be7b058 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/AbstractControllerTest.java @@ -0,0 +1,251 @@ +package ru.yandex.practicum.filmorate.controller; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import ru.yandex.practicum.filmorate.exception.ValidationException; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.User; + +import java.time.LocalDate; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Класс тестирования базового контроллера элементов + */ +class AbstractControllerTest { + + private AbstractController userController = new AbstractController<>(); + private AbstractController filmController = new AbstractController<>(); + + /** + * Очистка данных перед тестами + */ + @BeforeEach + void setUp() { + userController.clear(); + filmController.clear(); + } + + /** + * чтение фильма. + */ + @Test + void getFilm() { + createFilms(5); + Film film = filmController.getElement(1); + assertNotNull(film, "Фильм не читается."); + assertEquals(1, film.getId(), + "Фильм в начале списка не читается."); + + film = filmController.getElement(3); + assertEquals(3, film.getId(), + "Фильм в середине списка не читается."); + + film = filmController.getElement(5); + assertEquals(5, film.getId(), + "Фильм в конце списка не читается."); + + assertThrows(ValidationException.class, + () -> { + filmController.getElement(1000); + }, + "Попытка чтения несуществующего фильма должна приводить к исключению."); + } + + /** + * Чтение пользователя. + */ + @Test + void getUser() { + createUsers(5); + User user = userController.getElement(1); + assertNotNull(user, "Пользователь не читается."); + assertEquals(1, user.getId(), + "Пользователь в начале списка не читается."); + + user = userController.getElement(3); + assertEquals(3, user.getId(), + "Пользователь в середине списка не читается."); + + user = userController.getElement(5); + assertEquals(5, user.getId(), + "Пользователь в конце списка не читается."); + + assertThrows(ValidationException.class, + () -> { + userController.getElement(1000); + }, + "Попытка чтения несуществующего пользователя должна приводить к исключению."); + } + + /** + * Поиск всех фильмов. + */ + @Test + void findAllFilms() { + int filmsNumber = 4; + createFilms(filmsNumber); + Collection films = filmController.findAll(); + assertEquals(filmsNumber, films.size(), + "Список фильмов не читается."); + } + + /** + * Поиск всех пользователей. + */ + @Test + void findAllUsers() { + int usersNumber = 6; + createUsers(usersNumber); + Collection users = userController.findAll(); + assertEquals(usersNumber, users.size(), + "Список пользователей не читается."); + } + + /** + * Добавление нового фильма. + */ + @Test + void addNewFilm() { + Film film = new Film("Testing add film", + "Testing add film", + LocalDate.now().minusYears(10), + 120); + filmController.addNew(film); + + final Film filmFromController = new Film(filmController.getElement(1)); + + assertNotNull(filmFromController, "Фильм не читается."); + assertTrue(filmFromController.getName().equals("Testing add film"), + "Информация о фильме искажена."); + + assertThrows(ValidationException.class, + () -> { + filmController.addNew(filmFromController); + }, + "Попытка повторного добавления фильма должна приводить к исключению."); + } + + /** + * Добавление нового пользователя. + */ + @Test + void addNewUser() { + User user = new User("user123456@domain", + "user123456", + "Testing add user", + LocalDate.now().minusYears(18)); + userController.addNew(user); + + final User userFromController = userController.getElement(1); + assertNotNull(userFromController, "Пользователь не читается."); + assertTrue(userFromController.getEmail().equals("user123456@domain"), + "Информация о пользователе искажена."); + + assertThrows(ValidationException.class, + () -> { + userController.addNew(userFromController); + }, + "Попытка повторного добавления пользователя должна приводить к исключению."); + } + + /** + * Обновление фильма. + */ + @Test + void updateFilm() { + Film film = new Film("Testing film", + "Testing update film", + LocalDate.now().minusYears(10), + 120); + filmController.addNew(film); + + Film updFilm = new Film("Testing update film", + "Testing update film", + LocalDate.now().minusYears(10), + 100); + + updFilm.setId(1000); + + assertThrows(ValidationException.class, + () -> { + filmController.update(updFilm); + }, + "Попытка обновления несуществующего фильма должна приводить к исключению."); + + updFilm.setId(1); + filmController.update(updFilm); + + final Film filmFromController = filmController.getElement(1); + + assertNotNull(filmFromController, "Фильм не читается."); + assertTrue(filmFromController.getName().equals(updFilm.getName()), + "Информация о фильме не обновляется."); + } + + /** + * Обновление пользователя. + */ + @Test + void updateUser() { + User user = new User("user123456@domain", + "user123456", + "Testing update user", + LocalDate.now().minusYears(18)); + userController.addNew(user); + + User updUser = new User("user@update.domain", + "user098765", + "Testing update user", + LocalDate.now().minusYears(18)); + + // пытаемся обновить несуществующего пользователя + updUser.setId(1000); + + assertThrows(ValidationException.class, + () -> { + userController.update(updUser); + }, + "Попытка обновления несуществующего пользователя должна приводить к исключению."); + + updUser.setId(1); + userController.update(updUser); + + final User userFromController = userController.getElement(1); + assertNotNull(userFromController, "Обновленный пользователь не читается."); + assertTrue(userFromController.getEmail().equals(updUser.getEmail()), + "Информация о пользователе не обновляется."); + } + + /** + * Метод создания тестового массива фильмов + * + * @param count - число фильмов для генерации + */ + private void createFilms(int count) { + for (int i = 1; i <= count; i++) { + Film film = new Film("Film №" + i, + "testing film " + i, + LocalDate.now().minusYears(i), + i * 20); + filmController.addNew(film); + } + } + + /** + * Метод создания тестового массива пользоваелей + * + * @param count - число пользователей для генерации + */ + private void createUsers(int count) { + for (int i = 1; i <= count; i++) { + User user = new User("user" + i + "@domain", + "user" + i, + "testing user" + i, + LocalDate.now().minusYears(i * 3)); + userController.addNew(user); + } + } +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java new file mode 100644 index 0000000..7029917 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java @@ -0,0 +1,125 @@ +package ru.yandex.practicum.filmorate.controller; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.LocalDateAdapter; + +import java.time.LocalDate; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Тестируем контроллер запросов о фильмах + */ +@SpringBootTest +@AutoConfigureMockMvc +class FilmControllerTest { + @Autowired + MockMvc mvc; + + Gson gson = new GsonBuilder() + .setPrettyPrinting() + .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) + .create(); + + + /** + * Перед каждым тестом очищаем список фильмов. + */ + @BeforeEach + void setUp() throws Exception { + mvc.perform(delete("/films")) + .andExpect(status().isOk()); + } + + /** + * Тестируем режим поиска фильмов. + */ + @Test + void findAllFilms() throws Exception { + addNewFilm(); + mvc.perform(get("/films")) + .andExpect(status().isOk()); + } + + /** + * Тестируем добавление информации о новом фильме. + */ + @Test + void addNewFilm() throws Exception { + Film film = new Film("Film Test1", + "Testing addNewFilm", + LocalDate.now().minusYears(10), + 60); + String jsonString = gson.toJson(film); + + // При успешном добавлении фильма + // должен возвращаться статус 200 "Ok" + mvc.perform(post("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + // При повторном добавлении фильма + // должен возвращаться статус 400 "BadRequest" + mvc.perform(post("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + /** + * Тестируем обновление информации о фильме + */ + @Test + void updateFilm() throws Exception { + Film film = new Film("Film Test2", + "Testing updateFilm", + LocalDate.now().minusYears(10), + 60); + String jsonString = gson.toJson(film); + + // Добавляем тестовый фильм + mvc.perform(post("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + film.setDescription("Updated."); + jsonString = gson.toJson(film); + // При обновлении фильма с отсутствующим id + // должен возвращаться статус 400 "BadRequest" + mvc.perform(put("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + + film.setId(1000); + jsonString = gson.toJson(film); + // При обновлении фильма с неверным id + // должен возвращаться статус 404 "NotFound" + mvc.perform(put("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + + film.setId(1); + jsonString = gson.toJson(film); + // При обновлении фильма с корректным id + // должен возвращаться статус 200 "Ok" + mvc.perform(put("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java new file mode 100644 index 0000000..c849d81 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java @@ -0,0 +1,127 @@ +package ru.yandex.practicum.filmorate.controller; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import ru.yandex.practicum.filmorate.model.LocalDateAdapter; +import ru.yandex.practicum.filmorate.model.User; + +import java.time.LocalDate; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Тестируем контроллер запросов данных о пользователях + */ +@SpringBootTest +@AutoConfigureMockMvc +class UserControllerTest { + @Autowired + MockMvc mvc; + + Gson gson = new GsonBuilder() + .setPrettyPrinting() + .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) + .create(); + + /** + * Удаляем всех пользователей + */ + @BeforeEach + void setUp(/*@Autowired MockMvc mvc*/) throws Exception { + mvc.perform(delete("/users")) + .andExpect(status().isOk()); + } + + + /** + * Тестируем чтение списка пользователей + */ + @Test + void findAllUser() throws Exception { + addNewUser(); + + mvc.perform(get("/users")) + .andExpect(status().isOk()) // ожидается код статус 200 + .andDo(print()); + } + + /** + * Тестируем добавление нового пользователя + */ + @Test + void addNewUser() throws Exception { + User user = new User("User1234@domain", + "user1234", "testing user", + LocalDate.now().minusYears(22)); + String jsonString = gson.toJson(user); + + // При успешном добавлении пользователя + // должен возвращаться статус 200 "Ok" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + // Повторное добавление пользователя + // должно возвращать статус 400 "BadRequest" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + /** + * Тестирование обновления сведений о пользователе + */ + @Test + void updateUser() throws Exception { + User user = new User("User1234@domain", + "user0000", "testing user", + LocalDate.now().minusYears(22)); + String jsonString = gson.toJson(user); + + // Создаем тестового пользователя + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + user.setLogin("user12345"); + user.setName("Updated user."); + jsonString = gson.toJson(user); + + // Обновление записи без идентификатора + // должно возвращать статус 400 "BadRequest" + mvc.perform(put("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + user.setId(1000); + jsonString = gson.toJson(user); + // Обновление записи c несуществющим идентификатором + // должно возвращать статус 404 "NotFound" + mvc.perform(put("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + + user.setId(1); + jsonString = gson.toJson(user); + // Успешное обновление записи + // должно возвращать статус 200 "Ok" + mvc.perform(put("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java b/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java new file mode 100644 index 0000000..1a285c1 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java @@ -0,0 +1,134 @@ +package ru.yandex.practicum.filmorate.model; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDate; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Тестирование ограничений на значения полей класса Film + */ +@SpringBootTest +@AutoConfigureMockMvc +class FilmTest { + @Autowired + MockMvc mvc; + + Gson gson = new GsonBuilder() + .setPrettyPrinting() + .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) + .create(); + + /** + * Перед каждым тестом очищаем список фильмов. + */ + @BeforeEach + void setUp() throws Exception { + mvc.perform(delete("/films")) + .andExpect(status().isOk()); + } + + /** + * Проверка непустого названия фильма. + */ + @Test + void testName() throws Exception { + Film film = new Film("", + "Testing film.name", + LocalDate.now().minusYears(10), + 60); + String jsonString = gson.toJson(film); + // При добавлении фильма без названия + // должен возвращаться статус 400 "BadRequest" + mvc.perform(post("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + /** + * Проверка допусимого размера описания. + */ + @Test + void testDescription() throws Exception { + Film film = new Film("Film", + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890", + LocalDate.now().minusYears(10), + 60); + String jsonString = gson.toJson(film); + // При добавлении фильма + // должен возвращаться статус 400 "BadRequest" + mvc.perform(post("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andDo(print()); + + } + + /** + * Проверка допустимой даты выпуска фильма + */ + @Test + void testReleaseDate() throws Exception { + Film film = new Film("Film", + "Testing film.releaseDate", + LocalDate.now().plusDays(1), + 60); + String jsonString = gson.toJson(film); + // При добавлении фильма + // должен возвращаться статус 400 "BadRequest" + mvc.perform(post("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andDo(print()); + + film.setReleaseDate(LocalDate.of(1895, 12, 27)); + jsonString = gson.toJson(film); + // При добавлении фильма + // должен возвращаться статус 400 "BadRequest" + mvc.perform(post("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + /** + * Проверка допустимой длительности фильма + */ + @Test + void testDuration() throws Exception { + Film film = new Film("Film", + "Testing film.releaseDate", + LocalDate.now().minusYears(10), + 0); + String jsonString = gson.toJson(film); + // При добавлении фильма + // должен возвращаться статус 400 "BadRequest" + mvc.perform(post("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java b/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java new file mode 100644 index 0000000..bfe5c82 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java @@ -0,0 +1,208 @@ +package ru.yandex.practicum.filmorate.model; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDate; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Тестирование ограничений на значения полей класса User. + */ +@SpringBootTest +@AutoConfigureMockMvc +class UserTest { + @Autowired + MockMvc mvc; + + Gson gson = new GsonBuilder() + .setPrettyPrinting() + .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) + .create(); + + /** + * Удаляем всех пользователей + */ + @BeforeEach + void setUp(/*@Autowired MockMvc mvc*/) throws Exception { + mvc.perform(delete("/users")) + .andExpect(status().isOk()); + } + + /** + * Тестирование email пользователя + */ + @Test + void testEmail() throws Exception { + User user = new User(null, + "userTest", + "Testing user", + LocalDate.now().minusYears(32)); + String jsonString = gson.toJson(user); + + // Создание пользователя без email + // должно возвращать статус 400 "BadRequest" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + user.setEmail("user.domain@"); + jsonString = gson.toJson(user); + // Создание пользователя с неправильным email + // должно возвращать статус 400 "BadRequest" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + user.setEmail("user@domain"); + jsonString = gson.toJson(user); + // Создание пользователя с корректным email + // должно возвращать статус 200 "Ok" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + /** + * Тестирование login пользователя + */ + @Test + void testLogin() throws Exception { + User user = new User("user1234@test", + "", + "Testing user", + LocalDate.now().minusYears(32)); + String jsonString = gson.toJson(user); + + // Создание пользователя с пустым login + // должно возвращать статус 400 "BadRequest" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + user.setLogin("user test"); + jsonString = gson.toJson(user); + // Создание пользователя с login содержащим пробел + // должно возвращать статус 400 "BadRequest" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + user.setLogin("user1234"); + jsonString = gson.toJson(user); + // Создание пользователя с корректным login (содержит только латинские буквы и цифры) + // должно возвращать статус 200 "Ok" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + /** + * Тестируем корректность даты рождения + */ + @Test + void testBirthday() throws Exception { + User user = new User("user1234@test", + "user1234", + "Testing user", + LocalDate.now().plusDays(30)); + + String jsonString = gson.toJson(user); + // Создание пользователя с датой рождения в будущем + // должно возвращать статус 400 "BadRequest" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + user.setBirthday(LocalDate.now().minusYears(30)); + jsonString = gson.toJson(user); + // Создание тестового пользователя с корректной датой рождения + // должно возвращать статус 200 "Ok" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + /** + * Тестируем группу аннотаций для режима обновления данных + */ + @Test + void testUpdateUser() throws Exception { + User user = new User("user1234@test", + "user1234", + "Testing user", + LocalDate.now().minusYears(32)); + String jsonString = gson.toJson(user); + // Создание тестового пользователя + // должно возвращать статус 200 "Ok" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + jsonString = "{\"id\": 1, \"email\": \"user.domain@\"}"; + // Изменение пользователю email на некорректный + // должно возвращать статус 400 "BadRequest" + mvc.perform(put("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + jsonString = "{\"id\": 1, \"email\": \"user@host.domain\"}"; + // Изменение пользователю email на допустимый + // должно возвращать статус 200 "Ok" + mvc.perform(put("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + jsonString = "{\"id\": 1, \"login\": \"user test12\"}"; + // Изменение пользователю login на некорректный + // должно возвращать статус 400 "BadRequest" + mvc.perform(put("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + jsonString = "{\"id\": 1, \"login\": \"userTest\"}"; + // Изменение пользователю login на допустимый + // должно возвращать статус 200 "Ok" + mvc.perform(put("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + jsonString = "{\"id\": 1, \"birthday\": \"2050-01-01\"}"; + // Обновление пользователя с датой рождения в будущем + // должно возвращать статус 400 "BadRequest" + mvc.perform(put("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + jsonString = "{\"id\": 1, \"birthday\": \"2005-01-01\"}"; + // Обновление пользователя корректной датой рождения + // должно возвращать статус 200 "Ok" + mvc.perform(put("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } +} From 0dd656d51209b681b808dbcdb963ea1436df921b Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Sun, 22 Dec 2024 12:22:05 +0700 Subject: [PATCH 03/11] =?UTF-8?q?=D0=A1=D0=BF=D1=80=D0=B8=D0=BD=D1=82=2011?= =?UTF-8?q?.=20=D0=9D=D0=B0=D1=87=D0=B0=D0=BB=D0=BE.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 ++++- pom.xml | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b6cea1a..4a772d7 100644 --- a/README.md +++ b/README.md @@ -3,4 +3,7 @@ Созданиие прилощений на основе шаблона "Spring"

## Спринт 10 -Добавляем контроллеры фильмов и пользователей. \ No newline at end of file +Добавляем контроллеры фильмов и пользователей.

+ +## Спринт 11 +Добавляем работу с "друзьями" и "лайками" \ No newline at end of file diff --git a/pom.xml b/pom.xml index 596a9fc..0a23621 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,12 @@ spring-boot-starter-validation + + org.zalando + logbook-spring-boot-starter + 3.7.2 + + From 07a9514b37d4f927bea2606ba9e5cda1e2522798 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Sun, 22 Dec 2024 13:08:52 +0700 Subject: [PATCH 04/11] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=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=B4=D1=80=D1=83=D0=B7=D1=8C=D1=8F=D0=BC?= =?UTF-8?q?=D0=B8"=20=D0=B8=20"=D0=BB=D0=B0=D0=B9=D0=BA=D0=B0=D0=BC=D0=B8"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filmorate/controller/ErrorHandler.java | 33 ++-- .../filmorate/controller/FilmController.java | 91 ++++++---- .../filmorate/controller/UserController.java | 128 ++++++++++---- .../exception/NotFoundException.java | 10 ++ .../exception/ValidationException.java | 11 +- .../practicum/filmorate/model/Film.java | 4 + .../practicum/filmorate/model/User.java | 8 +- .../filmorate/service/FilmService.java | 127 ++++++++++++++ .../filmorate/service/UserService.java | 157 ++++++++++++++++++ .../InMemoryAbstractStorage.java} | 30 ++-- .../practicum/filmorate/storage/Storages.java | 32 ++++ .../filmorate/storage/film/FilmStorage.java | 27 +++ .../storage/film/InMemoryFilmStorage.java | 81 +++++++++ .../storage/user/InMemoryUserStorage.java | 70 ++++++++ .../filmorate/storage/user/UserStorage.java | 27 +++ src/main/resources/application.properties | 3 + 16 files changed, 731 insertions(+), 108 deletions(-) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/exception/NotFoundException.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/UserService.java rename src/main/java/ru/yandex/practicum/filmorate/{controller/AbstractController.java => storage/InMemoryAbstractStorage.java} (69%) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/Storages.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java b/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java index e65b2fc..3cae816 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java @@ -1,16 +1,15 @@ package ru.yandex.practicum.filmorate.controller; import jakarta.validation.ConstraintViolationException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.ErrorMessage; import ru.yandex.practicum.filmorate.model.ValidationErrorResponse; @@ -22,9 +21,9 @@ /** * Класс обработки исключений при обработке поступивших http запросов */ -@ControllerAdvice +@Slf4j +@RestControllerAdvice public class ErrorHandler { - private static final Logger log = LoggerFactory.getLogger(ErrorHandler.class); /** * Обработка исключения ConstraintViolationException - при проверке ограничений объекта @@ -32,7 +31,6 @@ public class ErrorHandler { * @param e - исключение * @return - список нарушений для отображения в теле ответа */ - @ResponseBody @ExceptionHandler(ConstraintViolationException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ValidationErrorResponse onConstraintValidationException(ConstraintViolationException e) { @@ -45,7 +43,7 @@ public ValidationErrorResponse onConstraintValidationException(ConstraintViolati ) .collect(Collectors.toList()); - log.info("404 {}.", e.getMessage()); + log.info("400 {}.", e.getMessage()); return new ValidationErrorResponse(violations); } @@ -57,14 +55,13 @@ public ValidationErrorResponse onConstraintValidationException(ConstraintViolati */ @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) - @ResponseBody public ValidationErrorResponse onMethodArgumentNotValidException( MethodArgumentNotValidException e ) { final List violations = e.getBindingResult().getFieldErrors().stream() .map(error -> new Violation(error.getField(), error.getDefaultMessage())) .collect(Collectors.toList()); - log.info("404 {}.", e.getMessage()); + log.info("400 {}.", e.getMessage()); return new ValidationErrorResponse(violations); } @@ -75,11 +72,17 @@ public ValidationErrorResponse onMethodArgumentNotValidException( * @return - объект для http ответа с сообщением об ошибке */ @ExceptionHandler(ValidationException.class) - public ResponseEntity onValidationException(ValidationException exception) { - log.info("{} {}.", exception.getHttpStatus(), exception.getMessage()); - return ResponseEntity - .status(exception.getHttpStatus()) - .body(new ErrorMessage(exception.getMessage())); + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorMessage onValidationException(ValidationException exception) { + log.info("400 {}.", exception.getMessage()); + return new ErrorMessage(exception.getMessage()); + } + + @ExceptionHandler(NotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorMessage notFoundObject(NotFoundException exception) { + log.info("404 {}.", exception.getMessage()); + return new ErrorMessage(exception.getMessage()); } /** diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java index 501097a..e0fc20a 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -1,21 +1,32 @@ package ru.yandex.practicum.filmorate.controller; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.Marker; +import ru.yandex.practicum.filmorate.service.FilmService; import java.util.Collection; +import java.util.Map; /** - * Класс обработки http запросов к фильмам. + * Класс обработки http запросов к информации о фильмах. */ +@Slf4j @RestController @RequestMapping("/films") -public class FilmController extends AbstractController { - private static final Logger log = LoggerFactory.getLogger(FilmController.class); +public class FilmController { + + @Autowired + FilmService service; + + @Autowired + public FilmController(FilmService service) { + this.service = service; + } /** * Метод поиска всех фильмов @@ -23,9 +34,28 @@ public class FilmController extends AbstractController { * @return - список фильмов */ @GetMapping + @ResponseStatus(HttpStatus.OK) public Collection findAllFilms() { - log.info("Get all films {}.", super.findAll().size()); - return super.findAll(); + log.info("Ищем все фильмы {}.", service.findAllFilms().size()); + return service.findAllFilms(); + } + + /** + * Метод поиска фильма по идентификатору + * + * @param id - идентификатор + * @return - найденный фильм + */ + @GetMapping("/{id}") + public Film findFilm(@PathVariable Integer id) { + log.info("Ищем фильм id={}.", id); + return service.getFilmById(id); + } + + @GetMapping("/popular") + public Collection findPopularFilms(@RequestParam(defaultValue = "10") int count) { + log.info("Ищем популярные {} фильмов.", count); + return service.findPopularFilms(count); } /** @@ -35,12 +65,12 @@ public Collection findAllFilms() { * @return - подтверждение добавленного объекта */ @PostMapping + @ResponseStatus(HttpStatus.CREATED) public Film addNewFilm(@Validated(Marker.OnBasic.class) @RequestBody Film film) { - log.info("Creating film: {}.", film.toString()); - return super.addNew(film); + log.info("Добавляем новй фильм: {}.", film.toString()); + return service.addNewFilm(film); } - /** * Метод обновления информации о фильме. * При вызове метода промзводится проверка аннотаций только для маркера OnUpdate.class. @@ -50,26 +80,29 @@ public Film addNewFilm(@Validated(Marker.OnBasic.class) @RequestBody Film film) * @return - подтверждение обновленного объекта */ @PutMapping + @ResponseStatus(HttpStatus.OK) public Film updateFilm(@Validated(Marker.OnUpdate.class) @RequestBody Film updFilm) { Integer id = updFilm.getId(); - Film film = new Film(getElement(id)); + log.info("Обновляем информацию о фильме id={} : {}", id, updFilm.toString()); + return service.updateFilm(updFilm); + } - // Обновляем информаию во временном объекте - if (updFilm.getName() != null) { - film.setName(updFilm.getName()); - } - if (updFilm.getDescription() != null) { - film.setDescription(updFilm.getDescription()); - } - if (updFilm.getReleaseDate() != null) { - film.setReleaseDate(updFilm.getReleaseDate()); - } - if (updFilm.getDuration() > 0) { - film.setDuration(updFilm.getDuration()); - } + @PutMapping("/{id}/like/{userId}") + @ResponseStatus(HttpStatus.OK) + public Map addLike(@PathVariable("id") Integer filmId, + @PathVariable("userId") Integer userId) { + log.debug("Добавляем \"лайк\" фильму {}, от пользователя {}.", filmId, userId); + service.addNewLike(filmId, userId); + return service.getFilmRank(filmId); + } - log.info("Updating film id={} : {}", id, film.toString()); - return super.update(film); + @DeleteMapping("/{id}/like/{userId}") + @ResponseStatus(HttpStatus.OK) + public Map removeLike(@PathVariable("id") Integer filmId, + @PathVariable("userId") Integer userId) { + log.debug("Удаляем \"лайк\" у фильма {}, от пользователя {}.", filmId, userId); + service.removeLike(filmId, userId); + return service.getFilmRank(filmId); } /** @@ -78,10 +111,10 @@ public Film updateFilm(@Validated(Marker.OnUpdate.class) @RequestBody Film updFi * @return - сообщение о выполнении */ @DeleteMapping + @ResponseStatus(HttpStatus.OK) public String onDelete() { - log.info("Deleting all films."); - clear(); - return "All films deleted."; + log.info("Удаляем все фильмы."); + return service.onDelete(); } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java index 36aaa72..c8a899c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -1,22 +1,31 @@ package ru.yandex.practicum.filmorate.controller; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.yandex.practicum.filmorate.model.Marker; import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.service.UserService; import java.util.Collection; /** * Класс обработки http запросов о пользователях. */ +@Slf4j @RestController @RequestMapping("/users") -@Validated -public class UserController extends AbstractController { - private static final Logger log = LoggerFactory.getLogger(UserController.class); +public class UserController { + + @Autowired + UserService service; + + @Autowired + public UserController(UserService service) { + this.service = service; + } /** * Метод поиска всех пользователей @@ -24,9 +33,48 @@ public class UserController extends AbstractController { * @return - список пользователей */ @GetMapping + @ResponseStatus(HttpStatus.OK) public Collection findAllUser() { - log.info("Get all users {}.", super.findAll().size()); - return super.findAll(); + log.info("Запрашиваем список всех пользователей {}.", service.findAllUsers().size()); + return service.findAllUsers(); + } + + /** + * Метод поиска пользователя по идентификатору + * + * @param id - идентификатор + * @return - найденный объект + */ + @GetMapping("/{id}") + public User findUser(@PathVariable Integer id) { + log.info("Ищем пользователя id={}.", id); + return service.getUserById(id); + } + + /** + * Поиск друзей у заданного пользователя + * + * @param id - идентификатор пользователя + * @return - список друзей пользователя + */ + @GetMapping("/{id}/friends") + public Collection findUsersFriends(@PathVariable Integer id) { + log.info("Ищем друзей пользователя id={}.", id); + return service.getUsersFriends(id); + } + + /** + * Метод поиска общих друзей у двух пользователей + * + * @param id - идентификатор пользователя + * @param otherId - идентификатор другого пользователя + * @return - список общих друзей + */ + @GetMapping("/{id}/friends/common/{otherId}") + public Collection findCommonFriends(@PathVariable("id") Integer id, + @PathVariable("otherId") Integer otherId) { + log.info("Ищем общих друзей пользователй: {}, {}.", id, otherId); + return service.getCommonFriends(id, otherId); } /** @@ -36,15 +84,10 @@ public Collection findAllUser() { * @return - подтверждение добавленного объекта */ @PostMapping + @ResponseStatus(HttpStatus.CREATED) public User addNewUser(@Validated(Marker.OnBasic.class) @RequestBody User user) { - // "имя для отображения может быть пустым - // — в таком случае будет использован логин" (ТЗ-№10) - if (user.getName() == null || user.getName().isBlank()) { - user.setName(user.getLogin()); - } - - log.info("Creating user : {}.", user.toString()); - return super.addNew(user); + log.info("Создаем пользователя : {}.", user.toString()); + return service.addNewUser(user); } /** @@ -56,26 +99,41 @@ public User addNewUser(@Validated(Marker.OnBasic.class) @RequestBody User user) * @return - подтверждение обновленного объекта */ @PutMapping + @ResponseStatus(HttpStatus.OK) public User updateUser(@Validated(Marker.OnUpdate.class) @RequestBody User updUser) { Integer id = updUser.getId(); - User user = new User(super.getElement(id)); + log.info("Обновляем данные о пользователе id={} : {}", id, updUser.toString()); + return service.updateUser(updUser); + } - // Обновляем информаию во временном объекте - if (updUser.getEmail() != null) { - user.setEmail(updUser.getEmail()); - } - if (updUser.getLogin() != null) { - user.setLogin(updUser.getLogin()); - } - if (updUser.getName() != null) { - user.setName(updUser.getName()); - } - if (updUser.getBirthday() != null) { - user.setBirthday(updUser.getBirthday()); - } + /** + * Метод добаления в "друзья" + * + * @param userId - идентификатор пользоателя + * @param friendId - идентификатор друга + * @return - сообщение о добавлении друга + */ + @PutMapping("/{userId}/friends/{friendId}") + @ResponseStatus(HttpStatus.OK) + public void addFriends(@PathVariable("userId") Integer userId, + @PathVariable("friendId") Integer friendId) { + log.info("Добавляем в \"друзья\" пользователей id1={}, id2={}", userId, friendId); + service.addFriends(userId, friendId); + } - log.info("Updating user id={} : {}", id, user.toString()); - return super.update(user); + /** + * Метод удаления пользователя из "друзей" + * + * @param id - идентификатор пользователя + * @param friendId - идентификатор друга + * @return - сообщение о подтверждении + */ + @DeleteMapping("/{id}/friends/{friendId}") + @ResponseStatus(HttpStatus.OK) + public void breakUpFriends(@PathVariable("id") Integer id, + @PathVariable("friendId") Integer friendId) { + log.info("Удаляем из \"друзей\" пользователей id1={}, id2={}", id, friendId); + service.breakUpFriends(id, friendId); } /** @@ -84,10 +142,10 @@ public User updateUser(@Validated(Marker.OnUpdate.class) @RequestBody User updUs * @return - сообщение о выполнении */ @DeleteMapping - public String onDelete() { - log.info("Deleting all users."); - clear(); - return "All users deleted."; + @ResponseStatus(HttpStatus.OK) + public String deleteAllUsers() { + log.info("Удаляем всех пользователей."); + return service.removeAllUsers(); } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/NotFoundException.java b/src/main/java/ru/yandex/practicum/filmorate/exception/NotFoundException.java new file mode 100644 index 0000000..eccaf6f --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/NotFoundException.java @@ -0,0 +1,10 @@ +package ru.yandex.practicum.filmorate.exception; + +/** + * Класс исключения при отсутствии искомой информации + */ +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/ValidationException.java b/src/main/java/ru/yandex/practicum/filmorate/exception/ValidationException.java index ff42ec6..d746e5e 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/exception/ValidationException.java +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ValidationException.java @@ -1,19 +1,10 @@ package ru.yandex.practicum.filmorate.exception; -import org.springframework.http.HttpStatus; - /** * класс исключений прии проверки допустимых значений переменнх */ public class ValidationException extends RuntimeException { - HttpStatus httpStatus; - - public ValidationException(HttpStatus httpStatus, String message) { + public ValidationException(String message) { super(message); - this.httpStatus = httpStatus; - } - - public HttpStatus getHttpStatus() { - return httpStatus; } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java index 41f650e..a7a9938 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -35,6 +35,8 @@ public class Film extends StorageData { groups = {Marker.OnBasic.class, Marker.OnUpdate.class}) private int duration; + private Integer rank = 0; + /** * Конструктор копирования сведений о фильме * @@ -46,5 +48,7 @@ public Film(Film original) { this.description = original.getDescription(); this.releaseDate = original.getReleaseDate(); this.duration = original.getDuration(); + this.rank = original.getRank(); } + } diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/User.java b/src/main/java/ru/yandex/practicum/filmorate/model/User.java index 04f123f..58cf9b4 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/User.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java @@ -1,6 +1,9 @@ package ru.yandex.practicum.filmorate.model; -import jakarta.validation.constraints.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.PastOrPresent; +import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; @@ -30,7 +33,8 @@ public class User extends StorageData { private String name; - @PastOrPresent(message = "Дата рождения не может быть в будущем.", groups = {Marker.OnBasic.class, Marker.OnUpdate.class}) + @PastOrPresent(message = "Дата рождения не может быть в будущем.", + groups = {Marker.OnBasic.class, Marker.OnUpdate.class}) private LocalDate birthday; /** diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java new file mode 100644 index 0000000..01a56b0 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -0,0 +1,127 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.storage.Storages; +import ru.yandex.practicum.filmorate.storage.film.FilmStorage; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Класс реализации запросов к информации о фильмах + */ +@Slf4j +@Service +public class FilmService { + + protected FilmStorage films = Storages.getFilmStorage(); + + /** + * Метод поиска всех фильмов + * + * @return - список фильмов + */ + public Collection findAllFilms() { + log.debug("Service: Ищем все фильмы {}.", films.findAllFilms().size()); + return films.findAllFilms(); + } + + /** + * Метод поиска фильма по идентификатору + * + * @param id - идентификатор + * @return - найденный фильм + */ + public Film getFilmById(Integer id) { + log.debug("Service: Ищем фильм id={}.", id); + return films.getFilmById(id); + } + + /** + * Метод добавления нового фильма. + * + * @param film - объект для добавления + * @return - подтверждение добавленного объекта + */ + public Film addNewFilm(Film film) { + film.setRank(0); // Для предотвращения ручного ввода рейтинга + log.debug("Service: Добавляем информацию о фильме: {}.", film.toString()); + return films.addNewFilm(film); + } + + /** + * Метод обновления информации о фильме. + * + * @param updFilm - объект с обновленной информацией о фильме + * @return - подтверждение обновленного объекта + */ + public Film updateFilm(Film updFilm) { + Integer id = updFilm.getId(); + Film film = new Film(films.getFilmById(id)); + + // Обновляем информаию во временном объекте + if (updFilm.getName() != null) { + film.setName(updFilm.getName()); + } + if (updFilm.getDescription() != null) { + film.setDescription(updFilm.getDescription()); + } + if (updFilm.getReleaseDate() != null) { + film.setReleaseDate(updFilm.getReleaseDate()); + } + if (updFilm.getDuration() > 0) { + film.setDuration(updFilm.getDuration()); + } + + log.debug("Service: Updating film id={} : {}", id, film.toString()); + return films.updateFilm(film); + } + + /** + * Удаление всех фильмов + * + * @return - сообщение о выполнении + */ + public String onDelete() { + log.debug("Service: Удаляем все фильмы."); + films.removeAllFilms(); + return "Все фильмы удалены."; + } + + public Integer addNewLike(Integer filmId, Integer userId) { + log.debug("Service: Добавляем \"лайк\" фильму {}, от пользователя {}.", filmId, userId); + if (UserService.users.getUserById(userId) == null) { + throw new NotFoundException("Не найден пользователь id=" + userId); + } + + films.getFilmById(filmId).setRank(films.addNewLike(filmId, userId)); + return films.getFilmById(filmId).getRank(); + } + + public Integer removeLike(Integer filmId, Integer userId) { + log.debug("Service: Удаляем \"лайк\" у фильма {}, от пользователя {}.", filmId, userId); + films.getFilmById(filmId).setRank(films.removeLike(filmId, userId)); + return films.getFilmById(filmId).getRank(); + } + + public Collection findPopularFilms(int count) { + List popularFilms = new ArrayList<>(); + popularFilms = films.findAllFilms().stream() + .sorted(Comparator.comparing(Film::getRank).reversed()) + .collect(Collectors.toList()); + if (count > popularFilms.size()) { + count = popularFilms.size(); + } + return popularFilms.subList(0, count); + } + + public Map getFilmRank(Integer filmId) { + Map response = new HashMap<>(); + response.put("Фильм :", films.getFilmById(filmId).toString()); + response.put("Рейтинг:", films.getFilmById(filmId).getRank().toString()); + return response; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java new file mode 100644 index 0000000..a05bd61 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -0,0 +1,157 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.Storages; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Класс реализации запросов к информации о пользователях + */ +@Slf4j +@Service +public class UserService { + + static protected UserStorage users = Storages.getUerStorage(); + + /** + * Метод поиска всех пользователей + * + * @return - список пользователей + */ + public Collection findAllUsers() { + log.debug("Sevice: Get all users {}.", users.findAllUsers().size()); + return users.findAllUsers(); + } + + /** + * Метод добавления нового пользователя. + * + * @param user - объект для добавления + * @return - подтверждение добавленного объекта + */ + public User addNewUser(User user) { + // "имя для отображения может быть пустым + // — в таком случае будет использован логин" (ТЗ-№10) + if (user.getName() == null | user.getName().isBlank()) { + user.setName(user.getLogin()); + } + + log.debug("Sevice: Creating user : {}.", user.toString()); + return users.addNewUser(user); + } + + /** + * Метод чтения информации о пользователе по заданному идентификатору + * + * @param id - идентификатор пользователя + * @return - найденный объект + */ + public User getUserById(Integer id) { + log.debug("Sevice: Get user {}.", id); + return users.getUserById(id); + } + + /** + * Метод обновления информации о пользователе. + * При вызове метода промзводится проверка аннотаций только для маркера OnUpdate.class. + * Кроме id любой другой параметр может отсутствовать + * + * @param updUser - объект с обновленной информацией о пользователе + * @return - обновленный объект + */ + public User updateUser(User updUser) { + Integer id = updUser.getId(); + User user = new User(users.getUserById(id)); + + // Обновляем информаию во временном объекте + if (updUser.getEmail() != null) { + user.setEmail(updUser.getEmail()); + } + if (updUser.getLogin() != null) { + user.setLogin(updUser.getLogin()); + } + if (updUser.getName() != null) { + user.setName(updUser.getName()); + } + if (updUser.getBirthday() != null) { + user.setBirthday(updUser.getBirthday()); + } + + log.debug("Sevice: Updating user id={} : {}", id, user.toString()); + return users.updateUser(user); + } + + /** + * Удаление всех пользователей + * + * @return - сообщение о выполнении + */ + public String removeAllUsers() { + log.debug("Sevice: Удаляем всех пользователей."); + users.removeAllUsers(); + return "Все пользователи удалены."; + } + + /** + * Медод добавления пользователей в друзья + * добавление в друзья происходит взаимное без подтверждений + * + * @param id1 - идентификатор пользователя + * @param id2 - идентификатор друга + */ + public void addFriends(Integer id1, Integer id2) { + // Добавление в друзья происходит без подтверждения. + // Еслb id1 дружит с id2, то автоматически id2 дружит с id1 + users.addFriend(id1, id2); + users.addFriend(id2, id1); + } + + /** + * Метод удаления пользователя из "друзей" + * + * @param id1 - идентификатор пользователя + * @param id2 - идентификатор друга + * @return - сообщение о подтверждении + */ + public void breakUpFriends(Integer id1, Integer id2) { + log.debug("Sevice: Удаляем из \"друзей\" пользователей {}, {}.", id1, id2); + users.breakUpFriends(id1, id2); + } + + /** + * Поиск всех друзей пользователя + * + * @param userId - идентификатор пользователя + * @return - список друзей + */ + public Collection getUsersFriends(Integer userId) { + List friends = new ArrayList<>(); + for (Integer friendId : users.findAllFriends(userId)) { + friends.add(users.getUserById(friendId)); + } + return friends; + } + + /** + * Метод поискаобщих друзей пользователей + * + * @param id1 - идентификатор пользователя + * @param id2 - идентификатор другого пользователя + * @return - список общих друзей + */ + public Collection getCommonFriends(Integer id1, Integer id2) { + List friendsId = users.findAllFriends(id1); + friendsId.retainAll(users.findAllFriends(id2)); + List friends = new ArrayList<>(); + for (Integer id : friendsId) { + friends.add(users.getUserById(id)); + } + return friends; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/AbstractController.java b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryAbstractStorage.java similarity index 69% rename from src/main/java/ru/yandex/practicum/filmorate/controller/AbstractController.java rename to src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryAbstractStorage.java index 3306c55..51f3c99 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/AbstractController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryAbstractStorage.java @@ -1,10 +1,7 @@ -package ru.yandex.practicum.filmorate.controller; +package ru.yandex.practicum.filmorate.storage; -import org.springframework.http.HttpStatus; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.RequestBody; +import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; -import ru.yandex.practicum.filmorate.model.Marker; import ru.yandex.practicum.filmorate.model.StorageData; import java.util.Collection; @@ -12,11 +9,11 @@ import java.util.Map; /** - * Класс работы с элементами хранилища информации + * Базовый класс работы с хранилищем в оперативной памяти * - * @param + * @param - класс описания элементов хранилища */ -public class AbstractController { +public class InMemoryAbstractStorage { private final Map storage = new HashMap<>(); /** @@ -25,11 +22,10 @@ public class AbstractController { * @param id - идентификатор элемента * @return - объект */ - public T getElement(final Integer id) { + public T getElement(final Integer id) throws NotFoundException { T element = storage.get(id); if (element == null) { - throw new ValidationException(HttpStatus.NOT_FOUND, - "Не найден Id=" + id); + throw new NotFoundException("Не найден Id=" + id); } return storage.get(id); } @@ -49,11 +45,10 @@ public Collection findAll() { * @param element - объект для добавления * @return - подтверждение добавленного объекта */ - public T addNew(@Validated @RequestBody T element) throws ValidationException { + public T addNew(T element) throws ValidationException { // Проверяем существование полльзователя для исключения дублирования if (storage.containsValue(element)) { - throw new ValidationException(HttpStatus.BAD_REQUEST, "Уже существует : " - + element.toString()); + throw new ValidationException("Уже существует : " + element.toString()); } element.setId(getNextId()); @@ -67,12 +62,13 @@ public T addNew(@Validated @RequestBody T element) throws ValidationException { * @param element - объект с обновленной информацией * @return - подтверждение обновленного объекта */ - public T update(@Validated(Marker.OnUpdate.class) @RequestBody T element) throws ValidationException { + public T update(T element) + throws NotFoundException { + Integer id = element.getId(); // проверяем необходимые условия if (!storage.containsKey(id)) { - throw new ValidationException(HttpStatus.NOT_FOUND, - "Не найден Id=" + id); + throw new NotFoundException("Не найден Id=" + id); } storage.put(id, element); return element; diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/Storages.java b/src/main/java/ru/yandex/practicum/filmorate/storage/Storages.java new file mode 100644 index 0000000..31814b9 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/Storages.java @@ -0,0 +1,32 @@ +package ru.yandex.practicum.filmorate.storage; + +import ru.yandex.practicum.filmorate.storage.film.FilmStorage; +import ru.yandex.practicum.filmorate.storage.film.InMemoryFilmStorage; +import ru.yandex.practicum.filmorate.storage.user.InMemoryUserStorage; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; + +/** + * Утилитарный класс для определения реализаций хранилищ данных + */ +public final class Storages { + private Storages() { + } + + /** + * Метод определения текущего хранилища информации о фильмах + * + * @return - актуальное хранилище + */ + public static FilmStorage getFilmStorage() { + return new InMemoryFilmStorage(); + } + + /** + * Метод определения текущего хранилища информации о пользователях + * + * @return - актуальное хранилище + */ + public static UserStorage getUerStorage() { + return new InMemoryUserStorage(); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java new file mode 100644 index 0000000..0698e26 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java @@ -0,0 +1,27 @@ +package ru.yandex.practicum.filmorate.storage.film; + +import ru.yandex.practicum.filmorate.model.Film; + +import java.util.Collection; + +public interface FilmStorage { + // добавление нового фильма + Film addNewFilm(Film newFilm); + + // чтение фильма по идентификатору + Film getFilmById(Integer id); + + // чтение всех фильмов + Collection findAllFilms(); + + // изменение сведений о фильме + Film updateFilm(Film updFilm); + + // добавление "лайка" к фильму + Integer addNewLike(Integer filmId, Integer userId); + + // удаление "лайка" к фильму + Integer removeLike(Integer filmId, Integer userId); + + void removeAllFilms(); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java new file mode 100644 index 0000000..9a1d452 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java @@ -0,0 +1,81 @@ +package ru.yandex.practicum.filmorate.storage.film; + +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.storage.InMemoryAbstractStorage; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +@Component +public class InMemoryFilmStorage extends InMemoryAbstractStorage implements FilmStorage { + + private final Map> likes = new HashMap<>(); + + @Override + public Film addNewFilm(Film newFilm) { + Film film = super.addNew(newFilm); + likes.put(film.getId(), new HashSet<>()); + return film; + } + + @Override + public Film getFilmById(Integer id) { + return super.getElement(id); + } + + @Override + public Collection findAllFilms() { + return super.findAll(); + } + + @Override + public Film updateFilm(Film updFilm) { + return super.update(updFilm); + } + + /** + * Добавление "лайка" к фильму. + * + * @param filmId - идентифмкатор фильма + * @param userId - идентификатор пользователя + * @return - число никальных лайков + */ + @Override + public Integer addNewLike(Integer filmId, Integer userId) { + if (likes.containsKey(filmId)) { + likes.get(filmId).add(userId); + } else { + throw new NotFoundException("Не найден фильм id=" + filmId); + } + return likes.get(filmId).size(); + } + + /** + * Удаление "лайка" у фильма + * + * @param filmId - идентификатор фильма + * @param userId - идентификатор пользователя + * @return - число независимых "лайков" у фильма + */ + @Override + public Integer removeLike(Integer filmId, Integer userId) { + if (likes.containsKey(filmId)) { + if (!likes.get(filmId).remove(userId)) { + throw new NotFoundException("Не найден \"лайк\" id=" + userId); + } + } else { + throw new NotFoundException("Не найден фильм id=" + filmId); + } + return likes.get(filmId).size(); + } + + @Override + public void removeAllFilms() { + likes.clear(); + super.clear(); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java new file mode 100644 index 0000000..491978a --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java @@ -0,0 +1,70 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.InMemoryAbstractStorage; + +import java.util.*; + +@Component +public class InMemoryUserStorage extends InMemoryAbstractStorage implements UserStorage { + + private final Map> friends = new HashMap<>(); + + + @Override + public User addNewUser(User newUser) { + User user = super.addNew(newUser); + friends.put(user.getId(), new HashSet<>()); + return user; + } + + @Override + public User getUserById(Integer id) { + return super.getElement(id); + } + + @Override + public Collection findAllUsers() { + return super.findAll(); + } + + @Override + public User updateUser(User updUser) { + return super.update(updUser); + } + + @Override + public void removeAllUsers() { + friends.clear(); + super.clear(); + } + + @Override + public void addFriend(Integer userId, Integer friendId) { + if ((getElement(userId) == null) || (getElement(friendId) == null)) { + throw new NotFoundException("Не найден один или оба друга. " + userId + "," + friendId); + } + friends.get(userId).add(friendId); + } + + @Override + public void breakUpFriends(Integer id1, Integer id2) { + if(getElement(id1) != null) { + friends.get(id1).remove(id2); + } + + if(getElement(id2) != null) { + friends.get(id2).remove(id1); + } + } + + @Override + public List findAllFriends(Integer userId) { + if (!friends.containsKey(userId)) { + throw new NotFoundException("Не найден id=" + userId); + } + return new ArrayList<>(friends.get(userId)); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java new file mode 100644 index 0000000..5e1d014 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java @@ -0,0 +1,27 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import ru.yandex.practicum.filmorate.model.User; + +import java.util.Collection; +import java.util.List; + +public interface UserStorage { + // добавление нового пользователя + User addNewUser(User newUser); + + // чтение пользователя по идентификатору + User getUserById(Integer id); + + // чтение всех пользователей + Collection findAllUsers(); + + User updateUser(User updUser); + + void removeAllUsers(); + + void addFriend(Integer userId, Integer friendId); + + void breakUpFriends(Integer id1, Integer id2); + + List findAllFriends(Integer userId); +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b13789..28aacc1 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,4 @@ +server.port=8080 + +logging.level.org.zalando.logbook: TRACE From 8fa7cf28c0abe8eb1153c7a94a41e1605c524c19 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Sun, 22 Dec 2024 18:15:17 +0700 Subject: [PATCH 05/11] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AbstractControllerTest.java | 251 ------------------ .../controller/FilmControllerTest.java | 94 ++++++- .../controller/UserControllerTest.java | 170 +++++++++++- .../filmorate/model/FilmApiTest.java | 136 ++++++++++ .../practicum/filmorate/model/FilmTest.java | 126 ++++----- .../filmorate/model/UserApiTest.java | 209 +++++++++++++++ .../practicum/filmorate/model/UserTest.java | 189 +++---------- .../filmorate/storage/StoragesTest.java | 33 +++ 8 files changed, 718 insertions(+), 490 deletions(-) delete mode 100644 src/test/java/ru/yandex/practicum/filmorate/controller/AbstractControllerTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/model/FilmApiTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/model/UserApiTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/StoragesTest.java diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/AbstractControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/AbstractControllerTest.java deleted file mode 100644 index be7b058..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/AbstractControllerTest.java +++ /dev/null @@ -1,251 +0,0 @@ -package ru.yandex.practicum.filmorate.controller; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import ru.yandex.practicum.filmorate.exception.ValidationException; -import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.model.User; - -import java.time.LocalDate; -import java.util.Collection; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Класс тестирования базового контроллера элементов - */ -class AbstractControllerTest { - - private AbstractController userController = new AbstractController<>(); - private AbstractController filmController = new AbstractController<>(); - - /** - * Очистка данных перед тестами - */ - @BeforeEach - void setUp() { - userController.clear(); - filmController.clear(); - } - - /** - * чтение фильма. - */ - @Test - void getFilm() { - createFilms(5); - Film film = filmController.getElement(1); - assertNotNull(film, "Фильм не читается."); - assertEquals(1, film.getId(), - "Фильм в начале списка не читается."); - - film = filmController.getElement(3); - assertEquals(3, film.getId(), - "Фильм в середине списка не читается."); - - film = filmController.getElement(5); - assertEquals(5, film.getId(), - "Фильм в конце списка не читается."); - - assertThrows(ValidationException.class, - () -> { - filmController.getElement(1000); - }, - "Попытка чтения несуществующего фильма должна приводить к исключению."); - } - - /** - * Чтение пользователя. - */ - @Test - void getUser() { - createUsers(5); - User user = userController.getElement(1); - assertNotNull(user, "Пользователь не читается."); - assertEquals(1, user.getId(), - "Пользователь в начале списка не читается."); - - user = userController.getElement(3); - assertEquals(3, user.getId(), - "Пользователь в середине списка не читается."); - - user = userController.getElement(5); - assertEquals(5, user.getId(), - "Пользователь в конце списка не читается."); - - assertThrows(ValidationException.class, - () -> { - userController.getElement(1000); - }, - "Попытка чтения несуществующего пользователя должна приводить к исключению."); - } - - /** - * Поиск всех фильмов. - */ - @Test - void findAllFilms() { - int filmsNumber = 4; - createFilms(filmsNumber); - Collection films = filmController.findAll(); - assertEquals(filmsNumber, films.size(), - "Список фильмов не читается."); - } - - /** - * Поиск всех пользователей. - */ - @Test - void findAllUsers() { - int usersNumber = 6; - createUsers(usersNumber); - Collection users = userController.findAll(); - assertEquals(usersNumber, users.size(), - "Список пользователей не читается."); - } - - /** - * Добавление нового фильма. - */ - @Test - void addNewFilm() { - Film film = new Film("Testing add film", - "Testing add film", - LocalDate.now().minusYears(10), - 120); - filmController.addNew(film); - - final Film filmFromController = new Film(filmController.getElement(1)); - - assertNotNull(filmFromController, "Фильм не читается."); - assertTrue(filmFromController.getName().equals("Testing add film"), - "Информация о фильме искажена."); - - assertThrows(ValidationException.class, - () -> { - filmController.addNew(filmFromController); - }, - "Попытка повторного добавления фильма должна приводить к исключению."); - } - - /** - * Добавление нового пользователя. - */ - @Test - void addNewUser() { - User user = new User("user123456@domain", - "user123456", - "Testing add user", - LocalDate.now().minusYears(18)); - userController.addNew(user); - - final User userFromController = userController.getElement(1); - assertNotNull(userFromController, "Пользователь не читается."); - assertTrue(userFromController.getEmail().equals("user123456@domain"), - "Информация о пользователе искажена."); - - assertThrows(ValidationException.class, - () -> { - userController.addNew(userFromController); - }, - "Попытка повторного добавления пользователя должна приводить к исключению."); - } - - /** - * Обновление фильма. - */ - @Test - void updateFilm() { - Film film = new Film("Testing film", - "Testing update film", - LocalDate.now().minusYears(10), - 120); - filmController.addNew(film); - - Film updFilm = new Film("Testing update film", - "Testing update film", - LocalDate.now().minusYears(10), - 100); - - updFilm.setId(1000); - - assertThrows(ValidationException.class, - () -> { - filmController.update(updFilm); - }, - "Попытка обновления несуществующего фильма должна приводить к исключению."); - - updFilm.setId(1); - filmController.update(updFilm); - - final Film filmFromController = filmController.getElement(1); - - assertNotNull(filmFromController, "Фильм не читается."); - assertTrue(filmFromController.getName().equals(updFilm.getName()), - "Информация о фильме не обновляется."); - } - - /** - * Обновление пользователя. - */ - @Test - void updateUser() { - User user = new User("user123456@domain", - "user123456", - "Testing update user", - LocalDate.now().minusYears(18)); - userController.addNew(user); - - User updUser = new User("user@update.domain", - "user098765", - "Testing update user", - LocalDate.now().minusYears(18)); - - // пытаемся обновить несуществующего пользователя - updUser.setId(1000); - - assertThrows(ValidationException.class, - () -> { - userController.update(updUser); - }, - "Попытка обновления несуществующего пользователя должна приводить к исключению."); - - updUser.setId(1); - userController.update(updUser); - - final User userFromController = userController.getElement(1); - assertNotNull(userFromController, "Обновленный пользователь не читается."); - assertTrue(userFromController.getEmail().equals(updUser.getEmail()), - "Информация о пользователе не обновляется."); - } - - /** - * Метод создания тестового массива фильмов - * - * @param count - число фильмов для генерации - */ - private void createFilms(int count) { - for (int i = 1; i <= count; i++) { - Film film = new Film("Film №" + i, - "testing film " + i, - LocalDate.now().minusYears(i), - i * 20); - filmController.addNew(film); - } - } - - /** - * Метод создания тестового массива пользоваелей - * - * @param count - число пользователей для генерации - */ - private void createUsers(int count) { - for (int i = 1; i <= count; i++) { - User user = new User("user" + i + "@domain", - "user" + i, - "testing user" + i, - LocalDate.now().minusYears(i * 3)); - userController.addNew(user); - } - } -} diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java index 7029917..96817a2 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java @@ -11,6 +11,7 @@ import org.springframework.test.web.servlet.MockMvc; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.LocalDateAdapter; +import ru.yandex.practicum.filmorate.model.User; import java.time.LocalDate; @@ -26,12 +27,11 @@ class FilmControllerTest { @Autowired MockMvc mvc; - Gson gson = new GsonBuilder() + static Gson gson = new GsonBuilder() .setPrettyPrinting() .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) .create(); - /** * Перед каждым тестом очищаем список фильмов. */ @@ -39,6 +39,20 @@ class FilmControllerTest { void setUp() throws Exception { mvc.perform(delete("/films")) .andExpect(status().isOk()); + + mvc.perform(delete("/users")) + .andExpect(status().isOk()); + + // Создадим одного пользователя для "лайков" + User user = new User("User1234@domain", + "user1234", "test user", + LocalDate.now().minusYears(22)); + + String jsonString = gson.toJson(user); + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()); } /** @@ -46,7 +60,7 @@ void setUp() throws Exception { */ @Test void findAllFilms() throws Exception { - addNewFilm(); + makeFilms(3); mvc.perform(get("/films")) .andExpect(status().isOk()); } @@ -59,7 +73,7 @@ void addNewFilm() throws Exception { Film film = new Film("Film Test1", "Testing addNewFilm", LocalDate.now().minusYears(10), - 60); + 60, 0); String jsonString = gson.toJson(film); // При успешном добавлении фильма @@ -67,7 +81,7 @@ void addNewFilm() throws Exception { mvc.perform(post("/films") .content(jsonString) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); + .andExpect(status().isCreated()); // При повторном добавлении фильма // должен возвращаться статус 400 "BadRequest" @@ -85,14 +99,14 @@ void updateFilm() throws Exception { Film film = new Film("Film Test2", "Testing updateFilm", LocalDate.now().minusYears(10), - 60); + 60, 0); String jsonString = gson.toJson(film); // Добавляем тестовый фильм mvc.perform(post("/films") .content(jsonString) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); + .andExpect(status().isCreated()); film.setDescription("Updated."); jsonString = gson.toJson(film); @@ -122,4 +136,70 @@ void updateFilm() throws Exception { .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); } + + /** + * Тестируем добавление "лайка" + * + * @throws Exception + */ + @Test + void addLike() throws Exception { + makeFilms(3); + + + // При добавлении "лайка" от несуществующего пользователя + // должен возвращаться статус 404 "NotFound" + mvc.perform(put("/films/1/like/1000") + .content("") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + + // При добавлении "лайка" + // должен возвращаться статус 200 "Ok" + mvc.perform(put("/films/2/like/1") + .content("") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + /** + * Тестируем удаление "лайка" + * + * @throws Exception + */ + @Test + void deleteLike() throws Exception { + addLike(); + + // При удалении "лайка" + // должен возвращаться статус 200 "Ok" + mvc.perform(delete("/films/2/like/1") + .content("") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + /** + * Генерация тестовых фильмов + * + * @param count - количество фильмов + * @throws Exception + */ + void makeFilms(int count) throws Exception { + StringBuilder fBuilder = new StringBuilder(); + fBuilder.append("{\"name\": \"Film%d\","); + fBuilder.append("\"description\": \"description%d\","); + fBuilder.append("\"releaseDate\": \"2000-01-%02d\","); + fBuilder.append("\"duration\": %d}"); + String formatStr = fBuilder.toString(); + + for (int i = 1; i <= count; i++) { + String jsonString = String.format(formatStr, i, i, i, i * 10); + mvc.perform(post("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()); + } + + } } diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java index c849d81..67c49b1 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java @@ -33,21 +33,20 @@ class UserControllerTest { .create(); /** - * Удаляем всех пользователей + * Перед каждым тестом удаляем всех пользователей */ @BeforeEach - void setUp(/*@Autowired MockMvc mvc*/) throws Exception { + void setUp() throws Exception { mvc.perform(delete("/users")) .andExpect(status().isOk()); } - /** * Тестируем чтение списка пользователей */ @Test void findAllUser() throws Exception { - addNewUser(); + makeUsers(3); mvc.perform(get("/users")) .andExpect(status().isOk()) // ожидается код статус 200 @@ -60,7 +59,7 @@ void findAllUser() throws Exception { @Test void addNewUser() throws Exception { User user = new User("User1234@domain", - "user1234", "testing user", + "user1234", "test user", LocalDate.now().minusYears(22)); String jsonString = gson.toJson(user); @@ -69,7 +68,7 @@ void addNewUser() throws Exception { mvc.perform(post("/users") .content(jsonString) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); + .andExpect(status().isCreated()); // Повторное добавление пользователя // должно возвращать статус 400 "BadRequest" @@ -93,7 +92,7 @@ void updateUser() throws Exception { mvc.perform(post("/users") .content(jsonString) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); + .andExpect(status().isCreated()); user.setLogin("user12345"); user.setName("Updated user."); @@ -124,4 +123,161 @@ void updateUser() throws Exception { .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); } + + /** + * Тестируем добавление друзей + * + * @throws Exception + */ + @Test + void addFriends() throws Exception { + makeUsers(3); + + // Объявление в "друзья" несуществующего пользователя + // должно возвращать статус 404 "NotFound" + mvc.perform(put("/users/1000/friends/1") + .content("") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + + // Объявление в "друзья" несуществующего друга + // должно возвращать статус 404 "NotFound()" + mvc.perform(put("/users/1/friends/1000") + .content("") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + + // Объявление в "друзья" сущществующих пользователей + // должно возвращать статус 200 "ok" + mvc.perform(put("/users/1/friends/2") + .content("") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + // Объявление в "друзья" сущществующих пользователей (граничный случай) + // должно возвращать статус 200 "ok" + mvc.perform(put("/users/3/friends/2") + .content("") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + /** + * Тестируем удаление друзей + * + * @throws Exception + */ + @Test + void removeFriends() throws Exception { + addFriends(); + + // Удаление из "друзьей" не сущществующих пользователей + // должно возвращать статус 404 "NotFound" + mvc.perform(delete("/users/1/friends/1000") + .content("") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + + // Удаление из "друзьей" сущществующих пользователей + // должно возвращать статус 200 "Ok" + mvc.perform(delete("/users/1/friends/2") + .content("") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + /** + * Тестируем чтение списка друзей + * + * @throws Exception + */ + @Test + void getFriends() throws Exception { + makeUsers(3); + + // Объявление в "друзья" + // должно возвращать статус 200 "ok" + mvc.perform(put("/users/1/friends/2") + .content("") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + // Объявление в "друзья" + // должно возвращать статус 200 "ok" + mvc.perform(put("/users/3/friends/2") + .content("") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + // читаем список "друзей", несуществующего пользователя + // должно возвращать статус 404 "NotFound" + mvc.perform(get("/users/2000/friends") + .content("") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + + + // читаем список "друзей" + // должно возвращать статус 200 "ok" + mvc.perform(get("/users/2/friends") + .content("") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + /** + * Тестируем поиск общих друзей + * + * @throws Exception + */ + @Test + void findCommonFrends() throws Exception { + makeUsers(3); + + // Объявление в "друзья" + // должно возвращать статус 200 "ok" + mvc.perform(put("/users/1/friends/2") + .content("") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + // Объявление в "друзья" + // должно возвращать статус 200 "ok" + mvc.perform(put("/users/3/friends/2") + .content("") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + // читаем список общих "друзей" + // должно возвращать статус 200 "ok" + mvc.perform(get("/users/1/friends/common/3") + .content("") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + /** + * Создание тестовых пользователей + * + * @param count - требуемое клличество тестовых пользователей + * @throws Exception + */ + void makeUsers(int count) throws Exception { + StringBuilder fBuilder = new StringBuilder(); + fBuilder.append("{\"email\": \"user000%d@domain\","); + fBuilder.append("\"login\": \"USER000%d\","); + fBuilder.append("\"name\": \"userName00%d\","); + fBuilder.append("\"birthday\": \"2000-01-%02d\"}"); + String formatStr = fBuilder.toString(); + + for (int i = 1; i <= count; i++) { + String jsonString = String.format(formatStr, i, i, i, i); + // При успешном добавлении пользователя + // должен возвращаться статус 200 "Ok" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()); + } + } } diff --git a/src/test/java/ru/yandex/practicum/filmorate/model/FilmApiTest.java b/src/test/java/ru/yandex/practicum/filmorate/model/FilmApiTest.java new file mode 100644 index 0000000..a72aa0e --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/model/FilmApiTest.java @@ -0,0 +1,136 @@ +package ru.yandex.practicum.filmorate.model; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDate; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Тестирование ограничений на значения полей класса Film при Http запросах + * Тестирование использования объектов в качестве параметров методов + */ +@SpringBootTest +@AutoConfigureMockMvc +class FilmApiTest { + @Autowired + private MockMvc mvc; + + private Gson gson = new GsonBuilder() + .setPrettyPrinting() + .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) + .create(); + + /** + * Перед каждым тестом очищаем список фильмов. + */ + @BeforeEach + void setUp() throws Exception { + mvc.perform(delete("/films")) + .andExpect(status().isOk()); + } + + /** + * Проверка непустого названия фильма. + */ + @Test + void testName() throws Exception { + Film film = new Film("", + "Testing film.name", + LocalDate.now().minusYears(10), + 60, 0); + String jsonString = gson.toJson(film); + // При добавлении фильма без названия + // должен возвращаться статус 400 "BadRequest" + mvc.perform(post("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + /** + * Проверка допусимого размера описания. + */ + @Test + void testDescription() throws Exception { + Film film = new Film("Film", + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890", + LocalDate.now().minusYears(10), + 60, 0); + String jsonString = gson.toJson(film); + // При добавлении фильма + // должен возвращаться статус 400 "BadRequest" + mvc.perform(post("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andDo(print()); + + } + + /** + * Проверка допустимой даты выпуска фильма + */ + @Test + void testReleaseDate() throws Exception { + Film film = new Film("Film", + "Testing film.releaseDate", + LocalDate.now().plusDays(1), + 60, 0); + String jsonString = gson.toJson(film); + // При добавлении фильма + // должен возвращаться статус 400 "BadRequest" + mvc.perform(post("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andDo(print()); + + film.setReleaseDate(LocalDate.of(1895, 12, 27)); + jsonString = gson.toJson(film); + // При добавлении фильма + // должен возвращаться статус 400 "BadRequest" + mvc.perform(post("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + /** + * Проверка допустимой длительности фильма + */ + @Test + void testDuration() throws Exception { + Film film = new Film("Film", + "Testing film.releaseDate", + LocalDate.now().minusYears(10), + 0, 0); + String jsonString = gson.toJson(film); + + // При добавлении фильма + // должен возвращаться статус 400 "BadRequest" + mvc.perform(post("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java b/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java index 1a285c1..f5da073 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java @@ -1,69 +1,53 @@ package ru.yandex.practicum.filmorate.model; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; import java.time.LocalDate; +import java.util.Set; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Тестирование ограничений на значения полей класса Film + * Автономный тест (Junit). */ -@SpringBootTest -@AutoConfigureMockMvc class FilmTest { - @Autowired - MockMvc mvc; - - Gson gson = new GsonBuilder() - .setPrettyPrinting() - .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) - .create(); + private Validator validator; /** - * Перед каждым тестом очищаем список фильмов. + * Перед каждым тестом готовим Validator */ @BeforeEach - void setUp() throws Exception { - mvc.perform(delete("/films")) - .andExpect(status().isOk()); + void setUp() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); } /** * Проверка непустого названия фильма. */ @Test - void testName() throws Exception { + void testName() { Film film = new Film("", "Testing film.name", LocalDate.now().minusYears(10), - 60); - String jsonString = gson.toJson(film); - // При добавлении фильма без названия - // должен возвращаться статус 400 "BadRequest" - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andDo(print()); + 60, 0); + + Set> violations = validator.validate(film, Marker.OnBasic.class); + assertFalse(violations.isEmpty()); } /** * Проверка допусимого размера описания. */ @Test - void testDescription() throws Exception { + void testDescription() { Film film = new Film("Film", "12345678901234567890123456789012345678901234567890" + "12345678901234567890123456789012345678901234567890" @@ -71,64 +55,58 @@ void testDescription() throws Exception { + "12345678901234567890123456789012345678901234567890" + "12345678901234567890123456789012345678901234567890", LocalDate.now().minusYears(10), - 60); - String jsonString = gson.toJson(film); - // При добавлении фильма - // должен возвращаться статус 400 "BadRequest" - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andDo(print()); + 60, 0); + Set> violations = validator.validate(film, Marker.OnBasic.class); + assertFalse(violations.isEmpty()); } /** * Проверка допустимой даты выпуска фильма */ @Test - void testReleaseDate() throws Exception { + void testReleaseDate() { Film film = new Film("Film", "Testing film.releaseDate", LocalDate.now().plusDays(1), - 60); - String jsonString = gson.toJson(film); - // При добавлении фильма - // должен возвращаться статус 400 "BadRequest" - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andDo(print()); + 60, 0); + + Set> violations = validator.validate(film, Marker.OnBasic.class); + assertFalse(violations.isEmpty()); film.setReleaseDate(LocalDate.of(1895, 12, 27)); - jsonString = gson.toJson(film); - // При добавлении фильма - // должен возвращаться статус 400 "BadRequest" - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andDo(print()); + + violations.clear(); + violations = validator.validate(film, Marker.OnBasic.class); + assertFalse(violations.isEmpty()); } /** * Проверка допустимой длительности фильма */ @Test - void testDuration() throws Exception { + void testDuration() { Film film = new Film("Film", - "Testing film.releaseDate", + "Testing film.duration", LocalDate.now().minusYears(10), - 0); - String jsonString = gson.toJson(film); - // При добавлении фильма - // должен возвращаться статус 400 "BadRequest" - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andDo(print()); + 0, 0); + + Set> violations = validator.validate(film, Marker.OnBasic.class); + assertFalse(violations.isEmpty()); + } + + /** + * Тестируем отсутствие ограничений при корректном создании фильма + */ + @Test + void testFilmOk() { + Film film = new Film("Film Ok", + "Testing film", + LocalDate.now().minusYears(10), + 60, 0); + + Set> violations = validator.validate(film, Marker.OnBasic.class); + assertTrue(violations.isEmpty(), violations.toString()); } -} +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/model/UserApiTest.java b/src/test/java/ru/yandex/practicum/filmorate/model/UserApiTest.java new file mode 100644 index 0000000..f14854a --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/model/UserApiTest.java @@ -0,0 +1,209 @@ +package ru.yandex.practicum.filmorate.model; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDate; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Тестирование ограничений на значения класса User при Http запросах. + * Тестирование использования объектов в качестве параметров методов + */ +@SpringBootTest +@AutoConfigureMockMvc +class UserApiTest { + @Autowired + private MockMvc mvc; + + private Gson gson = new GsonBuilder() + .setPrettyPrinting() + .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) + .create(); + + /** + * Перед каждым тестом удаляем всех пользователей + */ + @BeforeEach + void setUp(/*@Autowired MockMvc mvc*/) throws Exception { + mvc.perform(delete("/users")) + .andExpect(status().isOk()); + } + + /** + * Тестирование email пользователя + */ + @Test + void testEmail() throws Exception { + User user = new User(null, + "userTest", + "Testing user", + LocalDate.now().minusYears(32)); + String jsonString = gson.toJson(user); + + // Создание пользователя без email + // должно возвращать статус 400 "BadRequest" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + user.setEmail("user.domain@"); + jsonString = gson.toJson(user); + // Создание пользователя с неправильным email + // должно возвращать статус 400 "BadRequest" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + user.setEmail("user@domain"); + jsonString = gson.toJson(user); + // Создание пользователя с корректным email + // должно возвращать статус 201 "Created" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()); + } + + /** + * Тестирование login пользователя + */ + @Test + void testLogin() throws Exception { + User user = new User("user1234@test", + "", + "Testing user", + LocalDate.now().minusYears(32)); + String jsonString = gson.toJson(user); + + // Создание пользователя с пустым login + // должно возвращать статус 400 "BadRequest" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + user.setLogin("user test"); + jsonString = gson.toJson(user); + // Создание пользователя с login содержащим пробел + // должно возвращать статус 400 "BadRequest" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + user.setLogin("user1234"); + jsonString = gson.toJson(user); + // Создание пользователя с корректным login (содержит только латинские буквы и цифры) + // должно возвращать статус 201 "Created" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()); + } + + /** + * Тестируем корректность даты рождения + */ + @Test + void testBirthday() throws Exception { + User user = new User("user1234@test", + "user1234", + "Testing user", + LocalDate.now().plusDays(30)); + + String jsonString = gson.toJson(user); + // Создание пользователя с датой рождения в будущем + // должно возвращать статус 400 "BadRequest" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + user.setBirthday(LocalDate.now().minusYears(30)); + jsonString = gson.toJson(user); + // Создание тестового пользователя с корректной датой рождения + // должно возвращать статус 200 "Ok" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()); + } + + /** + * Тестируем группу аннотаций для режима обновления данных + */ + @Test + void testUpdateUser() throws Exception { + User user = new User("user1234@test", + "user1234", + "Testing user", + LocalDate.now().minusYears(32)); + String jsonString = gson.toJson(user); + // Создание тестового пользователя + // должно возвращать статус 201 "Created" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()); + + jsonString = "{\"id\": 1, \"email\": \"user.domain@\"}"; + // Изменение пользователю email на некорректный + // должно возвращать статус 400 "BadRequest" + mvc.perform(put("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + jsonString = "{\"id\": 1, \"email\": \"user@host.domain\"}"; + // Изменение пользователю email на допустимый + // должно возвращать статус 200 "Ok" + mvc.perform(put("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + jsonString = "{\"id\": 1, \"login\": \"user test12\"}"; + // Изменение пользователю login на некорректный + // должно возвращать статус 400 "BadRequest" + mvc.perform(put("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + jsonString = "{\"id\": 1, \"login\": \"userTest\"}"; + // Изменение пользователю login на допустимый + // должно возвращать статус 200 "Ok" + mvc.perform(put("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + jsonString = "{\"id\": 1, \"birthday\": \"2050-01-01\"}"; + // Обновление пользователя с датой рождения в будущем + // должно возвращать статус 400 "BadRequest" + mvc.perform(put("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + jsonString = "{\"id\": 1, \"birthday\": \"2005-01-01\"}"; + // Обновление пользователя корректной датой рождения + // должно возвращать статус 200 "Ok" + mvc.perform(put("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java b/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java index bfe5c82..2e61eb5 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java @@ -1,208 +1,95 @@ package ru.yandex.practicum.filmorate.model; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; import java.time.LocalDate; +import java.util.Set; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Тестирование ограничений на значения полей класса User. + * Автономный тест (Junit). */ -@SpringBootTest -@AutoConfigureMockMvc +// @SpringBootTest +// @AutoConfigureMockMvc class UserTest { - @Autowired - MockMvc mvc; - - Gson gson = new GsonBuilder() - .setPrettyPrinting() - .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) - .create(); + private Validator validator; /** - * Удаляем всех пользователей + * Перед каждым тестом готовим Validator */ @BeforeEach - void setUp(/*@Autowired MockMvc mvc*/) throws Exception { - mvc.perform(delete("/users")) - .andExpect(status().isOk()); + void setUp() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); } /** * Тестирование email пользователя */ @Test - void testEmail() throws Exception { - User user = new User(null, + void testInvalidEmail() throws Exception { + User user = new User("", "userTest", "Testing user", - LocalDate.now().minusYears(32)); - String jsonString = gson.toJson(user); - - // Создание пользователя без email - // должно возвращать статус 400 "BadRequest" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); + LocalDate.now().minusYears(22)); - user.setEmail("user.domain@"); - jsonString = gson.toJson(user); - // Создание пользователя с неправильным email - // должно возвращать статус 400 "BadRequest" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - user.setEmail("user@domain"); - jsonString = gson.toJson(user); - // Создание пользователя с корректным email - // должно возвращать статус 200 "Ok" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); + Set> violations = validator.validate(user, Marker.OnBasic.class); + assertFalse(violations.isEmpty()); } /** * Тестирование login пользователя */ @Test - void testLogin() throws Exception { + void testInvalidLogin() throws Exception { User user = new User("user1234@test", - "", + "", // login не должен быть пустым "Testing user", LocalDate.now().minusYears(32)); - String jsonString = gson.toJson(user); - - // Создание пользователя с пустым login - // должно возвращать статус 400 "BadRequest" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - user.setLogin("user test"); - jsonString = gson.toJson(user); - // Создание пользователя с login содержащим пробел - // должно возвращать статус 400 "BadRequest" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); + Set> violations = validator.validate(user, Marker.OnBasic.class); + assertFalse(violations.isEmpty()); - user.setLogin("user1234"); - jsonString = gson.toJson(user); - // Создание пользователя с корректным login (содержит только латинские буквы и цифры) - // должно возвращать статус 200 "Ok" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); + // login должен содержать только буквы и цифры + user.setLogin("yu%3242 @#"); + violations.clear(); + violations = validator.validate(user, Marker.OnBasic.class); + assertFalse(violations.isEmpty()); } /** * Тестируем корректность даты рождения */ @Test - void testBirthday() throws Exception { + void testInvalidBirthday() throws Exception { User user = new User("user1234@test", "user1234", "Testing user", - LocalDate.now().plusDays(30)); - - String jsonString = gson.toJson(user); - // Создание пользователя с датой рождения в будущем - // должно возвращать статус 400 "BadRequest" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); + LocalDate.now().plusDays(1)); // Дата рождения в будущем - user.setBirthday(LocalDate.now().minusYears(30)); - jsonString = gson.toJson(user); - // Создание тестового пользователя с корректной датой рождения - // должно возвращать статус 200 "Ok" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); + Set> violations = validator.validate(user, Marker.OnBasic.class); + assertFalse(violations.isEmpty()); } /** - * Тестируем группу аннотаций для режима обновления данных + * Тестируем отсутствие ошибок при корректном заполнение полей. */ @Test - void testUpdateUser() throws Exception { + void testUserOk() { User user = new User("user1234@test", "user1234", "Testing user", - LocalDate.now().minusYears(32)); - String jsonString = gson.toJson(user); - // Создание тестового пользователя - // должно возвращать статус 200 "Ok" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - - jsonString = "{\"id\": 1, \"email\": \"user.domain@\"}"; - // Изменение пользователю email на некорректный - // должно возвращать статус 400 "BadRequest" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - jsonString = "{\"id\": 1, \"email\": \"user@host.domain\"}"; - // Изменение пользователю email на допустимый - // должно возвращать статус 200 "Ok" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - - jsonString = "{\"id\": 1, \"login\": \"user test12\"}"; - // Изменение пользователю login на некорректный - // должно возвращать статус 400 "BadRequest" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - jsonString = "{\"id\": 1, \"login\": \"userTest\"}"; - // Изменение пользователю login на допустимый - // должно возвращать статус 200 "Ok" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - - jsonString = "{\"id\": 1, \"birthday\": \"2050-01-01\"}"; - // Обновление пользователя с датой рождения в будущем - // должно возвращать статус 400 "BadRequest" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); + LocalDate.now().minusYears(18)); - jsonString = "{\"id\": 1, \"birthday\": \"2005-01-01\"}"; - // Обновление пользователя корректной датой рождения - // должно возвращать статус 200 "Ok" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); + Set> violations = validator.validate(user, Marker.OnBasic.class); + assertTrue(violations.isEmpty(), violations.toString()); } } diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/StoragesTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/StoragesTest.java new file mode 100644 index 0000000..8f34c66 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/StoragesTest.java @@ -0,0 +1,33 @@ +package ru.yandex.practicum.filmorate.storage; + +import org.junit.jupiter.api.Test; +import ru.yandex.practicum.filmorate.storage.film.FilmStorage; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Тестироание утилитарного класса создания хранилищ + */ +class StoragesTest { + + /** + * Тестируем создание хранилища фильмов + */ + @Test + void getFilmStorage() { + FilmStorage filmStorage = Storages.getFilmStorage(); + assertNotNull(filmStorage, + "Хранилище фильмов не инициализируется."); + } + + /** + * Тестируем создание хранилища пользователей + */ + @Test + void getUerStorage() { + UserStorage userStorage = Storages.getUerStorage(); + assertNotNull(userStorage, + "Хранилище пользователей не инициализируется."); + } +} \ No newline at end of file From dc0f2ad7075ca2a7e2b10755cef75f6020f86329 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Sun, 22 Dec 2024 18:15:17 +0700 Subject: [PATCH 06/11] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BE=D0=BF=D0=B5=D1=87=D0=B0=D1=82?= =?UTF-8?q?=D0=BA=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4a772d7..3eee4ad 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # java-filmorate Учебный проект. -Созданиие прилощений на основе шаблона "Spring"

+Созданиие приложений на основе шаблона "Spring"
## Спринт 10 -Добавляем контроллеры фильмов и пользователей.

+Добавляем контроллеры фильмов и пользователей.
## Спринт 11 Добавляем работу с "друзьями" и "лайками" \ No newline at end of file From 945d3a3d521eccb73337d92399d5e9f473d2c00a Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Sun, 22 Dec 2024 18:28:44 +0700 Subject: [PATCH 07/11] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D1=82=D0=B8=D0=BB=D1=8F?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ru/yandex/practicum/filmorate/service/UserService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java index a05bd61..e7f15cf 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -17,7 +17,7 @@ @Service public class UserService { - static protected UserStorage users = Storages.getUerStorage(); + protected static UserStorage users = Storages.getUerStorage(); /** * Метод поиска всех пользователей From a4841e44ba379cb732474321749413e5d1ded1a3 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Sun, 22 Dec 2024 18:33:05 +0700 Subject: [PATCH 08/11] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D1=82=D0=B8=D0=BB=D1=8F?= =?UTF-8?q?=202.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../practicum/filmorate/storage/user/InMemoryUserStorage.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java index 491978a..db59361 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java @@ -51,11 +51,11 @@ public void addFriend(Integer userId, Integer friendId) { @Override public void breakUpFriends(Integer id1, Integer id2) { - if(getElement(id1) != null) { + if (getElement(id1) != null) { friends.get(id1).remove(id2); } - if(getElement(id2) != null) { + if (getElement(id2) != null) { friends.get(id2).remove(id1); } } From 2556dd4a8aa7aab17ae8ab1207277f666a0ade23 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Thu, 26 Dec 2024 22:48:04 +0700 Subject: [PATCH 09/11] =?UTF-8?q?=D0=98=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=B7=D0=B0=D0=BC=D0=B5=D1=87?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B9=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filmorate/controller/ErrorHandler.java | 12 ++ .../filmorate/controller/FilmController.java | 3 +- .../filmorate/controller/UserController.java | 3 +- .../practicum/filmorate/model/Film.java | 17 +-- .../filmorate/model/StorageData.java | 4 +- .../practicum/filmorate/model/User.java | 19 +--- .../filmorate/service/FilmService.java | 73 +++++++------ .../filmorate/service/UserService.java | 54 ++++++--- .../storage/InMemoryAbstractStorage.java | 97 ----------------- .../practicum/filmorate/storage/Storages.java | 32 ------ .../filmorate/storage/film/FilmStorage.java | 10 +- .../storage/film/InMemoryFilmStorage.java | 103 +++++++++++++----- .../storage/user/InMemoryUserStorage.java | 43 +++----- .../filmorate/storage/user/UserStorage.java | 5 +- .../controller/FilmControllerTest.java | 1 - .../filmorate/storage/StoragesTest.java | 33 ------ 16 files changed, 201 insertions(+), 308 deletions(-) delete mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryAbstractStorage.java delete mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/Storages.java delete mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/StoragesTest.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java b/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java index 3cae816..c3ef798 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java @@ -101,4 +101,16 @@ public ResponseEntity onHttpMessageNotReadableException( .body(new ErrorMessage("В запросе отсутствуют необходимые данные.")); } + /** + * Обработка непредвиденного исключения + * + * @param e - исключение + * @return - сообщение об ошибке + */ + @ExceptionHandler + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorMessage handleException(final Exception e) { + log.warn("Error", e); + return new ErrorMessage(e.getMessage()); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java index e0fc20a..e343f1a 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -20,8 +20,7 @@ @RequestMapping("/films") public class FilmController { - @Autowired - FilmService service; + private final FilmService service; @Autowired public FilmController(FilmService service) { diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java index c8a899c..250a3e4 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -19,8 +19,7 @@ @RequestMapping("/users") public class UserController { - @Autowired - UserService service; + private final UserService service; @Autowired public UserController(UserService service) { diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java index a7a9938..8f0d7f2 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -1,6 +1,7 @@ package ru.yandex.practicum.filmorate.model; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; @@ -20,6 +21,7 @@ @AllArgsConstructor @Validated public class Film extends StorageData { + @NotBlank(message = "Название фильма не может быть пустым.", groups = {Marker.OnBasic.class, Marker.OnUpdate.class}) private String name; @@ -36,19 +38,4 @@ public class Film extends StorageData { private int duration; private Integer rank = 0; - - /** - * Конструктор копирования сведений о фильме - * - * @param original - объект копирования - */ - public Film(Film original) { - this.id = original.getId(); - this.name = original.getName(); - this.description = original.getDescription(); - this.releaseDate = original.getReleaseDate(); - this.duration = original.getDuration(); - this.rank = original.getRank(); - } - } diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/StorageData.java b/src/main/java/ru/yandex/practicum/filmorate/model/StorageData.java index 33bf6de..5a8bb54 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/StorageData.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/StorageData.java @@ -4,10 +4,10 @@ import lombok.Data; /** - * Класс данных для наследования классов модел + * Класс данных для наследования классов модели */ @Data public class StorageData { @NotNull(groups = {Marker.OnUpdate.class}, message = "id должен быть определен") - Integer id; + protected Integer id; } diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/User.java b/src/main/java/ru/yandex/practicum/filmorate/model/User.java index 58cf9b4..436595d 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/User.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java @@ -1,9 +1,6 @@ package ru.yandex.practicum.filmorate.model; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.PastOrPresent; -import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.*; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; @@ -21,6 +18,7 @@ @AllArgsConstructor @Validated public class User extends StorageData { + @NotBlank(message = "Email не может быть пустым", groups = Marker.OnBasic.class) @Email(message = "Email должен удовлетворять правилам формирования почтовых адресов.", groups = {Marker.OnBasic.class, Marker.OnUpdate.class}) @@ -36,17 +34,4 @@ public class User extends StorageData { @PastOrPresent(message = "Дата рождения не может быть в будущем.", groups = {Marker.OnBasic.class, Marker.OnUpdate.class}) private LocalDate birthday; - - /** - * Конструктор копирования сведений о пользователе - * - * @param original - объект копирования - */ - public User(User original) { - this.id = original.getId(); - this.email = original.getEmail(); - this.login = original.getLogin(); - this.name = original.getName(); - this.birthday = original.getBirthday(); - } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java index 01a56b0..fc5634c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -3,12 +3,14 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.storage.Storages; import ru.yandex.practicum.filmorate.storage.film.FilmStorage; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; -import java.util.*; -import java.util.stream.Collectors; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; /** * Класс реализации запросов к информации о фильмах @@ -17,7 +19,13 @@ @Service public class FilmService { - protected FilmStorage films = Storages.getFilmStorage(); + private final FilmStorage films; + private final UserStorage users; + + public FilmService(FilmStorage filmStorage, UserStorage users) { + this.films = filmStorage; + this.users = users; + } /** * Метод поиска всех фильмов @@ -25,7 +33,6 @@ public class FilmService { * @return - список фильмов */ public Collection findAllFilms() { - log.debug("Service: Ищем все фильмы {}.", films.findAllFilms().size()); return films.findAllFilms(); } @@ -36,8 +43,8 @@ public Collection findAllFilms() { * @return - найденный фильм */ public Film getFilmById(Integer id) { - log.debug("Service: Ищем фильм id={}.", id); - return films.getFilmById(id); + return films.getFilmById(id).orElseThrow(() -> + new NotFoundException("Не найден фильм id=" + id)); } /** @@ -47,8 +54,10 @@ public Film getFilmById(Integer id) { * @return - подтверждение добавленного объекта */ public Film addNewFilm(Film film) { - film.setRank(0); // Для предотвращения ручного ввода рейтинга - log.debug("Service: Добавляем информацию о фильме: {}.", film.toString()); + if (films.findAllFilms().contains(film)) { + throw new ValidationException("Фильм уже существует :" + + film.getName()); + } return films.addNewFilm(film); } @@ -60,7 +69,8 @@ public Film addNewFilm(Film film) { */ public Film updateFilm(Film updFilm) { Integer id = updFilm.getId(); - Film film = new Film(films.getFilmById(id)); + Film film = films.getFilmById(id).orElseThrow(() -> + new NotFoundException("Не найден фильм id=" + id)); // Обновляем информаию во временном объекте if (updFilm.getName() != null) { @@ -75,9 +85,7 @@ public Film updateFilm(Film updFilm) { if (updFilm.getDuration() > 0) { film.setDuration(updFilm.getDuration()); } - - log.debug("Service: Updating film id={} : {}", id, film.toString()); - return films.updateFilm(film); + return film; } /** @@ -86,42 +94,41 @@ public Film updateFilm(Film updFilm) { * @return - сообщение о выполнении */ public String onDelete() { - log.debug("Service: Удаляем все фильмы."); films.removeAllFilms(); return "Все фильмы удалены."; } public Integer addNewLike(Integer filmId, Integer userId) { - log.debug("Service: Добавляем \"лайк\" фильму {}, от пользователя {}.", filmId, userId); - if (UserService.users.getUserById(userId) == null) { - throw new NotFoundException("Не найден пользователь id=" + userId); - } + Film film = films.getFilmById(filmId).orElseThrow(() -> + new NotFoundException("Не найден фильм id=" + filmId)); + users.getUserById(userId).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + userId)); - films.getFilmById(filmId).setRank(films.addNewLike(filmId, userId)); - return films.getFilmById(filmId).getRank(); + film.setRank(films.addNewLike(filmId, userId)); + return film.getRank(); } public Integer removeLike(Integer filmId, Integer userId) { - log.debug("Service: Удаляем \"лайк\" у фильма {}, от пользователя {}.", filmId, userId); - films.getFilmById(filmId).setRank(films.removeLike(filmId, userId)); - return films.getFilmById(filmId).getRank(); + Film film = films.getFilmById(filmId).orElseThrow(() -> + new NotFoundException("Не найден фильм id=" + filmId)); + users.getUserById(userId).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + userId)); + + film.setRank(films.removeLike(filmId, userId)); + return film.getRank(); } public Collection findPopularFilms(int count) { - List popularFilms = new ArrayList<>(); - popularFilms = films.findAllFilms().stream() - .sorted(Comparator.comparing(Film::getRank).reversed()) - .collect(Collectors.toList()); - if (count > popularFilms.size()) { - count = popularFilms.size(); - } - return popularFilms.subList(0, count); + return films.findPopularFilms(count); } public Map getFilmRank(Integer filmId) { + Film film = films.getFilmById(filmId).orElseThrow(() -> + new NotFoundException("Не найден фильм id=" + filmId)); + Map response = new HashMap<>(); - response.put("Фильм :", films.getFilmById(filmId).toString()); - response.put("Рейтинг:", films.getFilmById(filmId).getRank().toString()); + response.put("Фильм ", film.getName()); + response.put("Рейтинг", film.getRank().toString()); return response; } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java index e7f15cf..462dc78 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -1,9 +1,11 @@ package ru.yandex.practicum.filmorate.service; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.User; -import ru.yandex.practicum.filmorate.storage.Storages; import ru.yandex.practicum.filmorate.storage.user.UserStorage; import java.util.ArrayList; @@ -17,15 +19,18 @@ @Service public class UserService { - protected static UserStorage users = Storages.getUerStorage(); + private final UserStorage users; + @Autowired + public UserService(UserStorage userStorage) { + this.users = userStorage; + } /** * Метод поиска всех пользователей * * @return - список пользователей */ public Collection findAllUsers() { - log.debug("Sevice: Get all users {}.", users.findAllUsers().size()); return users.findAllUsers(); } @@ -41,8 +46,10 @@ public User addNewUser(User user) { if (user.getName() == null | user.getName().isBlank()) { user.setName(user.getLogin()); } - - log.debug("Sevice: Creating user : {}.", user.toString()); + if (users.findAllUsers().contains(user)) { + throw new ValidationException("Пользователь уже существует " + + user.getEmail()); + } return users.addNewUser(user); } @@ -53,8 +60,9 @@ public User addNewUser(User user) { * @return - найденный объект */ public User getUserById(Integer id) { - log.debug("Sevice: Get user {}.", id); - return users.getUserById(id); + User user = users.getUserById(id).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id)); + return user; } /** @@ -67,7 +75,8 @@ public User getUserById(Integer id) { */ public User updateUser(User updUser) { Integer id = updUser.getId(); - User user = new User(users.getUserById(id)); + User user = users.getUserById(id).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id)); // Обновляем информаию во временном объекте if (updUser.getEmail() != null) { @@ -82,9 +91,7 @@ public User updateUser(User updUser) { if (updUser.getBirthday() != null) { user.setBirthday(updUser.getBirthday()); } - - log.debug("Sevice: Updating user id={} : {}", id, user.toString()); - return users.updateUser(user); + return user; } /** @@ -106,6 +113,11 @@ public String removeAllUsers() { * @param id2 - идентификатор друга */ public void addFriends(Integer id1, Integer id2) { + users.getUserById(id1).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id1)); + users.getUserById(id2).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id2)); + // Добавление в друзья происходит без подтверждения. // Еслb id1 дружит с id2, то автоматически id2 дружит с id1 users.addFriend(id1, id2); @@ -120,7 +132,11 @@ public void addFriends(Integer id1, Integer id2) { * @return - сообщение о подтверждении */ public void breakUpFriends(Integer id1, Integer id2) { - log.debug("Sevice: Удаляем из \"друзей\" пользователей {}, {}.", id1, id2); + users.getUserById(id1).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id1)); + users.getUserById(id2).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id2)); + users.breakUpFriends(id1, id2); } @@ -131,26 +147,34 @@ public void breakUpFriends(Integer id1, Integer id2) { * @return - список друзей */ public Collection getUsersFriends(Integer userId) { + users.getUserById(userId).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + userId)); + List friends = new ArrayList<>(); for (Integer friendId : users.findAllFriends(userId)) { - friends.add(users.getUserById(friendId)); + friends.add(users.getUserById(friendId).get()); } return friends; } /** - * Метод поискаобщих друзей пользователей + * Метод поиска общих друзей пользователей * * @param id1 - идентификатор пользователя * @param id2 - идентификатор другого пользователя * @return - список общих друзей */ public Collection getCommonFriends(Integer id1, Integer id2) { + users.getUserById(id1).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id1)); + users.getUserById(id2).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id2)); + List friendsId = users.findAllFriends(id1); friendsId.retainAll(users.findAllFriends(id2)); List friends = new ArrayList<>(); for (Integer id : friendsId) { - friends.add(users.getUserById(id)); + friends.add(users.getUserById(id).get()); } return friends; } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryAbstractStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryAbstractStorage.java deleted file mode 100644 index 51f3c99..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryAbstractStorage.java +++ /dev/null @@ -1,97 +0,0 @@ -package ru.yandex.practicum.filmorate.storage; - -import ru.yandex.practicum.filmorate.exception.NotFoundException; -import ru.yandex.practicum.filmorate.exception.ValidationException; -import ru.yandex.practicum.filmorate.model.StorageData; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -/** - * Базовый класс работы с хранилищем в оперативной памяти - * - * @param - класс описания элементов хранилища - */ -public class InMemoryAbstractStorage { - private final Map storage = new HashMap<>(); - - /** - * Метод чтения элемента из хранилища - * - * @param id - идентификатор элемента - * @return - объект - */ - public T getElement(final Integer id) throws NotFoundException { - T element = storage.get(id); - if (element == null) { - throw new NotFoundException("Не найден Id=" + id); - } - return storage.get(id); - } - - /** - * Метод поиска всех элементов - * - * @return - список элементов - */ - public Collection findAll() { - return storage.values(); - } - - /** - * Метод добавления нового элемента - * - * @param element - объект для добавления - * @return - подтверждение добавленного объекта - */ - public T addNew(T element) throws ValidationException { - // Проверяем существование полльзователя для исключения дублирования - if (storage.containsValue(element)) { - throw new ValidationException("Уже существует : " + element.toString()); - } - - element.setId(getNextId()); - storage.put(element.getId(), element); - return element; - } - - /** - * Метод обновления элемента. - * - * @param element - объект с обновленной информацией - * @return - подтверждение обновленного объекта - */ - public T update(T element) - throws NotFoundException { - - Integer id = element.getId(); - // проверяем необходимые условия - if (!storage.containsKey(id)) { - throw new NotFoundException("Не найден Id=" + id); - } - storage.put(id, element); - return element; - } - - /** - * Вспомогательный метод для генерации идентификатора нового элемента - * - * @return - актуальный идентификатор - */ - private Integer getNextId() { - Integer currentMaxId = storage.keySet() - .stream() - .mapToInt(id -> id) - .max() - .orElse(0); - return ++currentMaxId; - } - - /** - * Метод очистки хранилища - */ - public void clear() { - storage.clear(); - } -} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/Storages.java b/src/main/java/ru/yandex/practicum/filmorate/storage/Storages.java deleted file mode 100644 index 31814b9..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/Storages.java +++ /dev/null @@ -1,32 +0,0 @@ -package ru.yandex.practicum.filmorate.storage; - -import ru.yandex.practicum.filmorate.storage.film.FilmStorage; -import ru.yandex.practicum.filmorate.storage.film.InMemoryFilmStorage; -import ru.yandex.practicum.filmorate.storage.user.InMemoryUserStorage; -import ru.yandex.practicum.filmorate.storage.user.UserStorage; - -/** - * Утилитарный класс для определения реализаций хранилищ данных - */ -public final class Storages { - private Storages() { - } - - /** - * Метод определения текущего хранилища информации о фильмах - * - * @return - актуальное хранилище - */ - public static FilmStorage getFilmStorage() { - return new InMemoryFilmStorage(); - } - - /** - * Метод определения текущего хранилища информации о пользователях - * - * @return - актуальное хранилище - */ - public static UserStorage getUerStorage() { - return new InMemoryUserStorage(); - } -} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java index 0698e26..826408c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java @@ -3,19 +3,23 @@ import ru.yandex.practicum.filmorate.model.Film; import java.util.Collection; +import java.util.Optional; public interface FilmStorage { // добавление нового фильма Film addNewFilm(Film newFilm); // чтение фильма по идентификатору - Film getFilmById(Integer id); + Optional getFilmById(Integer id); - // чтение всех фильмов + // поиск всех фильмов Collection findAllFilms(); + // поиск самых популярных фильмов + Collection findPopularFilms(int count); + // изменение сведений о фильме - Film updateFilm(Film updFilm); + void updateFilm(Film updFilm); // добавление "лайка" к фильму Integer addNewLike(Integer filmId, Integer userId); diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java index 9a1d452..814cfc0 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java @@ -1,40 +1,42 @@ package ru.yandex.practicum.filmorate.storage.film; import org.springframework.stereotype.Component; -import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.storage.InMemoryAbstractStorage; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; +import java.util.*; @Component -public class InMemoryFilmStorage extends InMemoryAbstractStorage implements FilmStorage { +public class InMemoryFilmStorage implements FilmStorage { + private final Map films = new HashMap<>(); private final Map> likes = new HashMap<>(); + private final List filmsRating = new ArrayList<>(); + Integer filmId = 0; @Override - public Film addNewFilm(Film newFilm) { - Film film = super.addNew(newFilm); - likes.put(film.getId(), new HashSet<>()); + public Film addNewFilm(Film film) { + filmId++; + film.setId(filmId); + film.setRank(0); + films.put(filmId, film); + likes.put(filmId, new HashSet<>()); + filmsRating.add(film); return film; } @Override - public Film getFilmById(Integer id) { - return super.getElement(id); + public Optional getFilmById(Integer id) { + return Optional.ofNullable(films.get(id)); } @Override public Collection findAllFilms() { - return super.findAll(); + return films.values(); } @Override - public Film updateFilm(Film updFilm) { - return super.update(updFilm); + public void updateFilm(Film updFilm) { + films.put(updFilm.getId(), updFilm); } /** @@ -46,11 +48,10 @@ public Film updateFilm(Film updFilm) { */ @Override public Integer addNewLike(Integer filmId, Integer userId) { - if (likes.containsKey(filmId)) { - likes.get(filmId).add(userId); - } else { - throw new NotFoundException("Не найден фильм id=" + filmId); - } + likes.get(filmId).add(userId); + Film film = films.get(filmId); + film.setRank(likes.get(filmId).size()); + setFilmsRating(film); return likes.get(filmId).size(); } @@ -63,19 +64,65 @@ public Integer addNewLike(Integer filmId, Integer userId) { */ @Override public Integer removeLike(Integer filmId, Integer userId) { - if (likes.containsKey(filmId)) { - if (!likes.get(filmId).remove(userId)) { - throw new NotFoundException("Не найден \"лайк\" id=" + userId); - } - } else { - throw new NotFoundException("Не найден фильм id=" + filmId); - } + likes.get(filmId).remove(userId); + Film film = films.get(filmId); + film.setRank(likes.get(filmId).size()); + setFilmsRating(film); return likes.get(filmId).size(); } + /** + * Определение позиции фильма в рейтинге. + * Так как рейтинг представляет собой уже упорядоченный список, + * то сортировать весь список нет смысла. + * Нужно уточнить место в рейтинге заданного объекта. + * + * @param film + */ + private void setFilmsRating(Film film) { + int ratingSize = filmsRating.size(); + + // Если фильмов меньше двух, то ничего не делаем + if (ratingSize < 2) { + return; + } + int index = filmsRating.indexOf(film); + + // Проверяем изменение рейтинга на возрастание + while ((index > 0) && + (film.getRank() > filmsRating.get(index - 1).getRank())) { + filmsRating.set(index, filmsRating.get(index - 1)); + filmsRating.set(--index, film); + } + + // Проверяем изменение рейтинга на убывание + while (index < (ratingSize - 1) && + (film.getRank() < filmsRating.get(index + 1).getRank())) { + filmsRating.set(index, filmsRating.get(index + 1)); + filmsRating.set(++index, film); + } + } + + /** + * Поиск самых популярных фильмов + * + * @param count - количество фильмов для поиска + * @return - список фильмов + */ + @Override + public Collection findPopularFilms(int count) { + if (count > filmsRating.size()) { + count = filmsRating.size(); + } + return filmsRating.subList(0, count); + + } + @Override public void removeAllFilms() { likes.clear(); - super.clear(); + filmsRating.clear(); + films.clear(); + filmId = 0; } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java index db59361..6e9f9fa 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java @@ -1,70 +1,61 @@ package ru.yandex.practicum.filmorate.storage.user; import org.springframework.stereotype.Component; -import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.model.User; -import ru.yandex.practicum.filmorate.storage.InMemoryAbstractStorage; import java.util.*; @Component -public class InMemoryUserStorage extends InMemoryAbstractStorage implements UserStorage { +public class InMemoryUserStorage implements UserStorage { + private final Map users = new HashMap<>(); private final Map> friends = new HashMap<>(); - + private Integer userId = 0; @Override - public User addNewUser(User newUser) { - User user = super.addNew(newUser); - friends.put(user.getId(), new HashSet<>()); + public User addNewUser(User user) { + userId++; + user.setId(userId); + users.put(userId, user); + friends.put(userId, new HashSet<>()); return user; } @Override - public User getUserById(Integer id) { - return super.getElement(id); + public Optional getUserById(Integer id) { + return Optional.ofNullable(users.get(id)); } @Override public Collection findAllUsers() { - return super.findAll(); + return users.values(); } @Override - public User updateUser(User updUser) { - return super.update(updUser); + public void updateUser(User updUser) { + users.put(updUser.getId(), updUser); } @Override public void removeAllUsers() { friends.clear(); - super.clear(); + users.clear(); + userId = 0; } @Override public void addFriend(Integer userId, Integer friendId) { - if ((getElement(userId) == null) || (getElement(friendId) == null)) { - throw new NotFoundException("Не найден один или оба друга. " + userId + "," + friendId); - } friends.get(userId).add(friendId); } @Override public void breakUpFriends(Integer id1, Integer id2) { - if (getElement(id1) != null) { - friends.get(id1).remove(id2); - } - - if (getElement(id2) != null) { - friends.get(id2).remove(id1); - } + friends.get(id1).remove(id2); + friends.get(id2).remove(id1); } @Override public List findAllFriends(Integer userId) { - if (!friends.containsKey(userId)) { - throw new NotFoundException("Не найден id=" + userId); - } return new ArrayList<>(friends.get(userId)); } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java index 5e1d014..3fa46c4 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java @@ -4,18 +4,19 @@ import java.util.Collection; import java.util.List; +import java.util.Optional; public interface UserStorage { // добавление нового пользователя User addNewUser(User newUser); // чтение пользователя по идентификатору - User getUserById(Integer id); + Optional getUserById(Integer id); // чтение всех пользователей Collection findAllUsers(); - User updateUser(User updUser); + void updateUser(User updUser); void removeAllUsers(); diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java index 96817a2..cc3de5e 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java @@ -146,7 +146,6 @@ void updateFilm() throws Exception { void addLike() throws Exception { makeFilms(3); - // При добавлении "лайка" от несуществующего пользователя // должен возвращаться статус 404 "NotFound" mvc.perform(put("/films/1/like/1000") diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/StoragesTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/StoragesTest.java deleted file mode 100644 index 8f34c66..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/StoragesTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package ru.yandex.practicum.filmorate.storage; - -import org.junit.jupiter.api.Test; -import ru.yandex.practicum.filmorate.storage.film.FilmStorage; -import ru.yandex.practicum.filmorate.storage.user.UserStorage; - -import static org.junit.jupiter.api.Assertions.assertNotNull; - -/** - * Тестироание утилитарного класса создания хранилищ - */ -class StoragesTest { - - /** - * Тестируем создание хранилища фильмов - */ - @Test - void getFilmStorage() { - FilmStorage filmStorage = Storages.getFilmStorage(); - assertNotNull(filmStorage, - "Хранилище фильмов не инициализируется."); - } - - /** - * Тестируем создание хранилища пользователей - */ - @Test - void getUerStorage() { - UserStorage userStorage = Storages.getUerStorage(); - assertNotNull(userStorage, - "Хранилище пользователей не инициализируется."); - } -} \ No newline at end of file From b11fa69b2fe80fe4bc27922f92868de2241da4a2 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Thu, 26 Dec 2024 22:50:45 +0700 Subject: [PATCH 10/11] =?UTF-8?q?=D0=98=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=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=D1=81=D1=82=D0=B8=D0=BB?= =?UTF-8?q?=D1=8F.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ru/yandex/practicum/filmorate/model/Film.java | 1 - src/main/java/ru/yandex/practicum/filmorate/model/User.java | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java index 8f0d7f2..9fd9b6b 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -1,7 +1,6 @@ package ru.yandex.practicum.filmorate.model; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/User.java b/src/main/java/ru/yandex/practicum/filmorate/model/User.java index 436595d..fd9ad3f 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/User.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java @@ -1,6 +1,9 @@ package ru.yandex.practicum.filmorate.model; -import jakarta.validation.constraints.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.PastOrPresent; +import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; From a20c9d2c20dc0b68c3d6e37362713b25cbdb86bd Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Thu, 26 Dec 2024 22:50:45 +0700 Subject: [PATCH 11/11] =?UTF-8?q?=D0=98=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=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=D1=81=D1=82=D0=B8=D0=BB?= =?UTF-8?q?=D1=8F.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ru/yandex/practicum/filmorate/service/UserService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java index 462dc78..6007441 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -25,6 +25,7 @@ public class UserService { public UserService(UserStorage userStorage) { this.users = userStorage; } + /** * Метод поиска всех пользователей *