diff --git a/README.md b/README.md
index 8d38b6d..3eee4ad 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,9 @@
# java-filmorate
Учебный проект.
-Созданиие прилощений на основе шаблона "Spring"
\ No newline at end of file
+Созданиие приложений на основе шаблона "Spring"
+
+## Спринт 10
+Добавляем контроллеры фильмов и пользователей.
+
+## Спринт 11
+Добавляем работу с "друзьями" и "лайками"
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 0cad031..0a23621 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,24 @@
lombok
provided
+
org.springframework.boot
spring-boot-starter-test
test
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ org.zalando
+ logbook-spring-boot-starter
+ 3.7.2
+
+
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/ErrorHandler.java b/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java
new file mode 100644
index 0000000..c3ef798
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java
@@ -0,0 +1,116 @@
+package ru.yandex.practicum.filmorate.controller;
+
+import jakarta.validation.ConstraintViolationException;
+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.ExceptionHandler;
+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;
+import ru.yandex.practicum.filmorate.model.Violation;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Класс обработки исключений при обработке поступивших http запросов
+ */
+@Slf4j
+@RestControllerAdvice
+public class ErrorHandler {
+
+ /**
+ * Обработка исключения ConstraintViolationException - при проверке ограничений объекта
+ *
+ * @param e - исключение
+ * @return - список нарушений для отображения в теле ответа
+ */
+ @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("400 {}.", e.getMessage());
+ return new ValidationErrorResponse(violations);
+ }
+
+ /**
+ * Обработка исключения MethodArgumentNotValidException - при проверке аргумента метода
+ *
+ * @param e - исключение
+ * @return - список нарушений для отображения в теле ответа
+ */
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ 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("400 {}.", e.getMessage());
+ return new ValidationErrorResponse(violations);
+ }
+
+ /**
+ * Метод обработки пользовательского исключения ValidationException
+ *
+ * @param exception - исключкние проверки данных
+ * @return - объект для http ответа с сообщением об ошибке
+ */
+ @ExceptionHandler(ValidationException.class)
+ @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());
+ }
+
+ /**
+ * Обработка исключения 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("В запросе отсутствуют необходимые данные."));
+ }
+
+ /**
+ * Обработка непредвиденного исключения
+ *
+ * @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 08cf0a1..e343f1a 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,119 @@
package ru.yandex.practicum.filmorate.controller;
-import org.springframework.web.bind.annotation.RestController;
+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 запросов к информации о фильмах.
+ */
+@Slf4j
@RestController
+@RequestMapping("/films")
public class FilmController {
+
+ private final FilmService service;
+
+ @Autowired
+ public FilmController(FilmService service) {
+ this.service = service;
+ }
+
+ /**
+ * Метод поиска всех фильмов
+ *
+ * @return - список фильмов
+ */
+ @GetMapping
+ @ResponseStatus(HttpStatus.OK)
+ public Collection findAllFilms() {
+ 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);
+ }
+
+ /**
+ * Метод добавления нового фильма.
+ *
+ * @param film - объект для добавления
+ * @return - подтверждение добавленного объекта
+ */
+ @PostMapping
+ @ResponseStatus(HttpStatus.CREATED)
+ public Film addNewFilm(@Validated(Marker.OnBasic.class) @RequestBody Film film) {
+ log.info("Добавляем новй фильм: {}.", film.toString());
+ return service.addNewFilm(film);
+ }
+
+ /**
+ * Метод обновления информации о фильме.
+ * При вызове метода промзводится проверка аннотаций только для маркера OnUpdate.class.
+ * Кроме id любой другой параметр может отсутствовать
+ *
+ * @param updFilm - объект с обновленной информацией о фильме
+ * @return - подтверждение обновленного объекта
+ */
+ @PutMapping
+ @ResponseStatus(HttpStatus.OK)
+ public Film updateFilm(@Validated(Marker.OnUpdate.class) @RequestBody Film updFilm) {
+ Integer id = updFilm.getId();
+ log.info("Обновляем информацию о фильме id={} : {}", id, updFilm.toString());
+ return service.updateFilm(updFilm);
+ }
+
+ @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);
+ }
+
+ @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);
+ }
+
+ /**
+ * Удаление всех фильмов
+ *
+ * @return - сообщение о выполнении
+ */
+ @DeleteMapping
+ @ResponseStatus(HttpStatus.OK)
+ public String onDelete() {
+ 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
new file mode 100644
index 0000000..250a3e4
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java
@@ -0,0 +1,150 @@
+package ru.yandex.practicum.filmorate.controller;
+
+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")
+public class UserController {
+
+ private final UserService service;
+
+ @Autowired
+ public UserController(UserService service) {
+ this.service = service;
+ }
+
+ /**
+ * Метод поиска всех пользователей
+ *
+ * @return - список пользователей
+ */
+ @GetMapping
+ @ResponseStatus(HttpStatus.OK)
+ public Collection findAllUser() {
+ 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);
+ }
+
+ /**
+ * Метод добавления нового пользователя.
+ *
+ * @param user - объект для добавления
+ * @return - подтверждение добавленного объекта
+ */
+ @PostMapping
+ @ResponseStatus(HttpStatus.CREATED)
+ public User addNewUser(@Validated(Marker.OnBasic.class) @RequestBody User user) {
+ log.info("Создаем пользователя : {}.", user.toString());
+ return service.addNewUser(user);
+ }
+
+ /**
+ * Метод обновления информации о пользователе.
+ * При вызове метода промзводится проверка аннотаций только для маркера OnUpdate.class.
+ * Кроме id любой другой параметр может отсутствовать
+ *
+ * @param updUser - объект с обновленной информацией о пользователе
+ * @return - подтверждение обновленного объекта
+ */
+ @PutMapping
+ @ResponseStatus(HttpStatus.OK)
+ public User updateUser(@Validated(Marker.OnUpdate.class) @RequestBody User updUser) {
+ Integer id = updUser.getId();
+ log.info("Обновляем данные о пользователе id={} : {}", id, updUser.toString());
+ return service.updateUser(updUser);
+ }
+
+ /**
+ * Метод добаления в "друзья"
+ *
+ * @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);
+ }
+
+ /**
+ * Метод удаления пользователя из "друзей"
+ *
+ * @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);
+ }
+
+ /**
+ * Удаление всех пользователей
+ *
+ * @return - сообщение о выполнении
+ */
+ @DeleteMapping
+ @ResponseStatus(HttpStatus.OK)
+ public String deleteAllUsers() {
+ log.info("Удаляем всех пользователей.");
+ return service.removeAllUsers();
+ }
+
+}
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/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
new file mode 100644
index 0000000..d746e5e
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ValidationException.java
@@ -0,0 +1,10 @@
+package ru.yandex.practicum.filmorate.exception;
+
+/**
+ * класс исключений прии проверки допустимых значений переменнх
+ */
+public class ValidationException extends RuntimeException {
+ public ValidationException(String message) {
+ super(message);
+ }
+}
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..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,12 +1,40 @@
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;
+
+ private Integer rank = 0;
}
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..5a8bb54
--- /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 должен быть определен")
+ 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
new file mode 100644
index 0000000..fd9ad3f
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java
@@ -0,0 +1,40 @@
+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 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;
+}
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/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..fc5634c
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java
@@ -0,0 +1,134 @@
+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.exception.ValidationException;
+import ru.yandex.practicum.filmorate.model.Film;
+import ru.yandex.practicum.filmorate.storage.film.FilmStorage;
+import ru.yandex.practicum.filmorate.storage.user.UserStorage;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Класс реализации запросов к информации о фильмах
+ */
+@Slf4j
+@Service
+public class FilmService {
+
+ private final FilmStorage films;
+ private final UserStorage users;
+
+ public FilmService(FilmStorage filmStorage, UserStorage users) {
+ this.films = filmStorage;
+ this.users = users;
+ }
+
+ /**
+ * Метод поиска всех фильмов
+ *
+ * @return - список фильмов
+ */
+ public Collection findAllFilms() {
+ return films.findAllFilms();
+ }
+
+ /**
+ * Метод поиска фильма по идентификатору
+ *
+ * @param id - идентификатор
+ * @return - найденный фильм
+ */
+ public Film getFilmById(Integer id) {
+ return films.getFilmById(id).orElseThrow(() ->
+ new NotFoundException("Не найден фильм id=" + id));
+ }
+
+ /**
+ * Метод добавления нового фильма.
+ *
+ * @param film - объект для добавления
+ * @return - подтверждение добавленного объекта
+ */
+ public Film addNewFilm(Film film) {
+ if (films.findAllFilms().contains(film)) {
+ throw new ValidationException("Фильм уже существует :"
+ + film.getName());
+ }
+ return films.addNewFilm(film);
+ }
+
+ /**
+ * Метод обновления информации о фильме.
+ *
+ * @param updFilm - объект с обновленной информацией о фильме
+ * @return - подтверждение обновленного объекта
+ */
+ public Film updateFilm(Film updFilm) {
+ Integer id = updFilm.getId();
+ Film film = films.getFilmById(id).orElseThrow(() ->
+ new NotFoundException("Не найден фильм id=" + 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());
+ }
+ return film;
+ }
+
+ /**
+ * Удаление всех фильмов
+ *
+ * @return - сообщение о выполнении
+ */
+ public String onDelete() {
+ films.removeAllFilms();
+ return "Все фильмы удалены.";
+ }
+
+ public Integer addNewLike(Integer filmId, Integer userId) {
+ Film film = films.getFilmById(filmId).orElseThrow(() ->
+ new NotFoundException("Не найден фильм id=" + filmId));
+ users.getUserById(userId).orElseThrow(() ->
+ new NotFoundException("Не найден пользователь id=" + userId));
+
+ film.setRank(films.addNewLike(filmId, userId));
+ return film.getRank();
+ }
+
+ public Integer removeLike(Integer filmId, Integer userId) {
+ 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) {
+ 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("Фильм ", 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
new file mode 100644
index 0000000..6007441
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java
@@ -0,0 +1,182 @@
+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.user.UserStorage;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Класс реализации запросов к информации о пользователях
+ */
+@Slf4j
+@Service
+public class UserService {
+
+ private final UserStorage users;
+
+ @Autowired
+ public UserService(UserStorage userStorage) {
+ this.users = userStorage;
+ }
+
+ /**
+ * Метод поиска всех пользователей
+ *
+ * @return - список пользователей
+ */
+ public Collection findAllUsers() {
+ return users.findAllUsers();
+ }
+
+ /**
+ * Метод добавления нового пользователя.
+ *
+ * @param user - объект для добавления
+ * @return - подтверждение добавленного объекта
+ */
+ public User addNewUser(User user) {
+ // "имя для отображения может быть пустым
+ // — в таком случае будет использован логин" (ТЗ-№10)
+ if (user.getName() == null | user.getName().isBlank()) {
+ user.setName(user.getLogin());
+ }
+ if (users.findAllUsers().contains(user)) {
+ throw new ValidationException("Пользователь уже существует "
+ + user.getEmail());
+ }
+ return users.addNewUser(user);
+ }
+
+ /**
+ * Метод чтения информации о пользователе по заданному идентификатору
+ *
+ * @param id - идентификатор пользователя
+ * @return - найденный объект
+ */
+ public User getUserById(Integer id) {
+ User user = users.getUserById(id).orElseThrow(() ->
+ new NotFoundException("Не найден пользователь id=" + id));
+ return user;
+ }
+
+ /**
+ * Метод обновления информации о пользователе.
+ * При вызове метода промзводится проверка аннотаций только для маркера OnUpdate.class.
+ * Кроме id любой другой параметр может отсутствовать
+ *
+ * @param updUser - объект с обновленной информацией о пользователе
+ * @return - обновленный объект
+ */
+ public User updateUser(User updUser) {
+ Integer id = updUser.getId();
+ User user = users.getUserById(id).orElseThrow(() ->
+ new NotFoundException("Не найден пользователь id=" + 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());
+ }
+ return user;
+ }
+
+ /**
+ * Удаление всех пользователей
+ *
+ * @return - сообщение о выполнении
+ */
+ public String removeAllUsers() {
+ log.debug("Sevice: Удаляем всех пользователей.");
+ users.removeAllUsers();
+ return "Все пользователи удалены.";
+ }
+
+ /**
+ * Медод добавления пользователей в друзья
+ * добавление в друзья происходит взаимное без подтверждений
+ *
+ * @param id1 - идентификатор пользователя
+ * @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);
+ users.addFriend(id2, id1);
+ }
+
+ /**
+ * Метод удаления пользователя из "друзей"
+ *
+ * @param id1 - идентификатор пользователя
+ * @param id2 - идентификатор друга
+ * @return - сообщение о подтверждении
+ */
+ public void breakUpFriends(Integer id1, Integer id2) {
+ users.getUserById(id1).orElseThrow(() ->
+ new NotFoundException("Не найден пользователь id=" + id1));
+ users.getUserById(id2).orElseThrow(() ->
+ new NotFoundException("Не найден пользователь id=" + id2));
+
+ users.breakUpFriends(id1, id2);
+ }
+
+ /**
+ * Поиск всех друзей пользователя
+ *
+ * @param userId - идентификатор пользователя
+ * @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).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).get());
+ }
+ return friends;
+ }
+}
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..826408c
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java
@@ -0,0 +1,31 @@
+package ru.yandex.practicum.filmorate.storage.film;
+
+import ru.yandex.practicum.filmorate.model.Film;
+
+import java.util.Collection;
+import java.util.Optional;
+
+public interface FilmStorage {
+ // добавление нового фильма
+ Film addNewFilm(Film newFilm);
+
+ // чтение фильма по идентификатору
+ Optional getFilmById(Integer id);
+
+ // поиск всех фильмов
+ Collection findAllFilms();
+
+ // поиск самых популярных фильмов
+ Collection findPopularFilms(int count);
+
+ // изменение сведений о фильме
+ void 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..814cfc0
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java
@@ -0,0 +1,128 @@
+package ru.yandex.practicum.filmorate.storage.film;
+
+import org.springframework.stereotype.Component;
+import ru.yandex.practicum.filmorate.model.Film;
+
+import java.util.*;
+
+@Component
+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 film) {
+ filmId++;
+ film.setId(filmId);
+ film.setRank(0);
+ films.put(filmId, film);
+ likes.put(filmId, new HashSet<>());
+ filmsRating.add(film);
+ return film;
+ }
+
+ @Override
+ public Optional getFilmById(Integer id) {
+ return Optional.ofNullable(films.get(id));
+ }
+
+ @Override
+ public Collection findAllFilms() {
+ return films.values();
+ }
+
+ @Override
+ public void updateFilm(Film updFilm) {
+ films.put(updFilm.getId(), updFilm);
+ }
+
+ /**
+ * Добавление "лайка" к фильму.
+ *
+ * @param filmId - идентифмкатор фильма
+ * @param userId - идентификатор пользователя
+ * @return - число никальных лайков
+ */
+ @Override
+ public Integer addNewLike(Integer filmId, Integer userId) {
+ likes.get(filmId).add(userId);
+ Film film = films.get(filmId);
+ film.setRank(likes.get(filmId).size());
+ setFilmsRating(film);
+ return likes.get(filmId).size();
+ }
+
+ /**
+ * Удаление "лайка" у фильма
+ *
+ * @param filmId - идентификатор фильма
+ * @param userId - идентификатор пользователя
+ * @return - число независимых "лайков" у фильма
+ */
+ @Override
+ public Integer removeLike(Integer filmId, Integer userId) {
+ 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();
+ 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
new file mode 100644
index 0000000..6e9f9fa
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java
@@ -0,0 +1,61 @@
+package ru.yandex.practicum.filmorate.storage.user;
+
+import org.springframework.stereotype.Component;
+import ru.yandex.practicum.filmorate.model.User;
+
+import java.util.*;
+
+@Component
+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 user) {
+ userId++;
+ user.setId(userId);
+ users.put(userId, user);
+ friends.put(userId, new HashSet<>());
+ return user;
+ }
+
+ @Override
+ public Optional getUserById(Integer id) {
+ return Optional.ofNullable(users.get(id));
+ }
+
+ @Override
+ public Collection findAllUsers() {
+ return users.values();
+ }
+
+ @Override
+ public void updateUser(User updUser) {
+ users.put(updUser.getId(), updUser);
+ }
+
+ @Override
+ public void removeAllUsers() {
+ friends.clear();
+ users.clear();
+ userId = 0;
+ }
+
+ @Override
+ public void addFriend(Integer userId, Integer friendId) {
+ friends.get(userId).add(friendId);
+ }
+
+ @Override
+ public void breakUpFriends(Integer id1, Integer id2) {
+ friends.get(id1).remove(id2);
+ friends.get(id2).remove(id1);
+ }
+
+ @Override
+ public List findAllFriends(Integer 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..3fa46c4
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java
@@ -0,0 +1,28 @@
+package ru.yandex.practicum.filmorate.storage.user;
+
+import ru.yandex.practicum.filmorate.model.User;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+public interface UserStorage {
+ // добавление нового пользователя
+ User addNewUser(User newUser);
+
+ // чтение пользователя по идентификатору
+ Optional getUserById(Integer id);
+
+ // чтение всех пользователей
+ Collection findAllUsers();
+
+ void 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
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..cc3de5e
--- /dev/null
+++ b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java
@@ -0,0 +1,204 @@
+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 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.MockMvcResultMatchers.status;
+
+/**
+ * Тестируем контроллер запросов о фильмах
+ */
+@SpringBootTest
+@AutoConfigureMockMvc
+class FilmControllerTest {
+ @Autowired
+ MockMvc mvc;
+
+ static Gson gson = new GsonBuilder()
+ .setPrettyPrinting()
+ .registerTypeAdapter(LocalDate.class, new LocalDateAdapter())
+ .create();
+
+ /**
+ * Перед каждым тестом очищаем список фильмов.
+ */
+ @BeforeEach
+ 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());
+ }
+
+ /**
+ * Тестируем режим поиска фильмов.
+ */
+ @Test
+ void findAllFilms() throws Exception {
+ makeFilms(3);
+ 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, 0);
+ String jsonString = gson.toJson(film);
+
+ // При успешном добавлении фильма
+ // должен возвращаться статус 200 "Ok"
+ mvc.perform(post("/films")
+ .content(jsonString)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isCreated());
+
+ // При повторном добавлении фильма
+ // должен возвращаться статус 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, 0);
+ String jsonString = gson.toJson(film);
+
+ // Добавляем тестовый фильм
+ mvc.perform(post("/films")
+ .content(jsonString)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isCreated());
+
+ 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());
+ }
+
+ /**
+ * Тестируем добавление "лайка"
+ *
+ * @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
new file mode 100644
index 0000000..67c49b1
--- /dev/null
+++ b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java
@@ -0,0 +1,283 @@
+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() throws Exception {
+ mvc.perform(delete("/users"))
+ .andExpect(status().isOk());
+ }
+
+ /**
+ * Тестируем чтение списка пользователей
+ */
+ @Test
+ void findAllUser() throws Exception {
+ makeUsers(3);
+
+ mvc.perform(get("/users"))
+ .andExpect(status().isOk()) // ожидается код статус 200
+ .andDo(print());
+ }
+
+ /**
+ * Тестируем добавление нового пользователя
+ */
+ @Test
+ void addNewUser() throws Exception {
+ User user = new User("User1234@domain",
+ "user1234", "test user",
+ LocalDate.now().minusYears(22));
+ String jsonString = gson.toJson(user);
+
+ // При успешном добавлении пользователя
+ // должен возвращаться статус 200 "Ok"
+ mvc.perform(post("/users")
+ .content(jsonString)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isCreated());
+
+ // Повторное добавление пользователя
+ // должно возвращать статус 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().isCreated());
+
+ 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());
+ }
+
+ /**
+ * Тестируем добавление друзей
+ *
+ * @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
new file mode 100644
index 0000000..f5da073
--- /dev/null
+++ b/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java
@@ -0,0 +1,112 @@
+package ru.yandex.practicum.filmorate.model;
+
+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 java.time.LocalDate;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Тестирование ограничений на значения полей класса Film
+ * Автономный тест (Junit).
+ */
+class FilmTest {
+ private Validator validator;
+
+ /**
+ * Перед каждым тестом готовим Validator
+ */
+ @BeforeEach
+ void setUp() {
+ ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+ validator = factory.getValidator();
+ }
+
+ /**
+ * Проверка непустого названия фильма.
+ */
+ @Test
+ void testName() {
+ Film film = new Film("",
+ "Testing film.name",
+ LocalDate.now().minusYears(10),
+ 60, 0);
+
+ Set> violations = validator.validate(film, Marker.OnBasic.class);
+ assertFalse(violations.isEmpty());
+ }
+
+ /**
+ * Проверка допусимого размера описания.
+ */
+ @Test
+ void testDescription() {
+ Film film = new Film("Film",
+ "12345678901234567890123456789012345678901234567890"
+ + "12345678901234567890123456789012345678901234567890"
+ + "12345678901234567890123456789012345678901234567890"
+ + "12345678901234567890123456789012345678901234567890"
+ + "12345678901234567890123456789012345678901234567890",
+ LocalDate.now().minusYears(10),
+ 60, 0);
+
+ Set> violations = validator.validate(film, Marker.OnBasic.class);
+ assertFalse(violations.isEmpty());
+ }
+
+ /**
+ * Проверка допустимой даты выпуска фильма
+ */
+ @Test
+ void testReleaseDate() {
+ Film film = new Film("Film",
+ "Testing film.releaseDate",
+ LocalDate.now().plusDays(1),
+ 60, 0);
+
+ Set> violations = validator.validate(film, Marker.OnBasic.class);
+ assertFalse(violations.isEmpty());
+
+ film.setReleaseDate(LocalDate.of(1895, 12, 27));
+
+ violations.clear();
+ violations = validator.validate(film, Marker.OnBasic.class);
+ assertFalse(violations.isEmpty());
+ }
+
+ /**
+ * Проверка допустимой длительности фильма
+ */
+ @Test
+ void testDuration() {
+ Film film = new Film("Film",
+ "Testing film.duration",
+ LocalDate.now().minusYears(10),
+ 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
new file mode 100644
index 0000000..2e61eb5
--- /dev/null
+++ b/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java
@@ -0,0 +1,95 @@
+package ru.yandex.practicum.filmorate.model;
+
+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 java.time.LocalDate;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Тестирование ограничений на значения полей класса User.
+ * Автономный тест (Junit).
+ */
+// @SpringBootTest
+// @AutoConfigureMockMvc
+class UserTest {
+ private Validator validator;
+
+ /**
+ * Перед каждым тестом готовим Validator
+ */
+ @BeforeEach
+ void setUp() {
+ ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+ validator = factory.getValidator();
+ }
+
+ /**
+ * Тестирование email пользователя
+ */
+ @Test
+ void testInvalidEmail() throws Exception {
+ User user = new User("",
+ "userTest",
+ "Testing user",
+ LocalDate.now().minusYears(22));
+
+ Set> violations = validator.validate(user, Marker.OnBasic.class);
+ assertFalse(violations.isEmpty());
+ }
+
+ /**
+ * Тестирование login пользователя
+ */
+ @Test
+ void testInvalidLogin() throws Exception {
+ User user = new User("user1234@test",
+ "", // login не должен быть пустым
+ "Testing user",
+ LocalDate.now().minusYears(32));
+
+ Set> violations = validator.validate(user, Marker.OnBasic.class);
+ assertFalse(violations.isEmpty());
+
+ // login должен содержать только буквы и цифры
+ user.setLogin("yu%3242 @#");
+ violations.clear();
+ violations = validator.validate(user, Marker.OnBasic.class);
+ assertFalse(violations.isEmpty());
+ }
+
+ /**
+ * Тестируем корректность даты рождения
+ */
+ @Test
+ void testInvalidBirthday() throws Exception {
+ User user = new User("user1234@test",
+ "user1234",
+ "Testing user",
+ LocalDate.now().plusDays(1)); // Дата рождения в будущем
+
+ Set> violations = validator.validate(user, Marker.OnBasic.class);
+ assertFalse(violations.isEmpty());
+ }
+
+ /**
+ * Тестируем отсутствие ошибок при корректном заполнение полей.
+ */
+ @Test
+ void testUserOk() {
+ User user = new User("user1234@test",
+ "user1234",
+ "Testing user",
+ LocalDate.now().minusYears(18));
+
+ Set> violations = validator.validate(user, Marker.OnBasic.class);
+ assertTrue(violations.isEmpty(), violations.toString());
+ }
+}