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()); + } +}