diff --git a/pom.xml b/pom.xml index 10fbe05..001dcbd 100644 --- a/pom.xml +++ b/pom.xml @@ -41,6 +41,11 @@ gson 2.8.9 + + org.zalando + logbook-spring-boot-starter + 3.7.2 + 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 fc08be6..901f812 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -2,8 +2,10 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.response.SuccessResponse; import ru.yandex.practicum.filmorate.service.FilmService; import java.util.Collection; @@ -17,16 +19,39 @@ public class FilmController { @GetMapping public Collection getFilms() { - return filmService.getFilms(); + return filmService.getAllFilms(); + } + + @GetMapping("/{filmId}") + public Film getFilm(@PathVariable Long filmId) { + return filmService.getFilmById(filmId); } @PostMapping + @ResponseStatus(HttpStatus.CREATED) public Film addFilm(@Valid @RequestBody Film film) { - return filmService.addFilm(film); + return filmService.createFilm(film); } @PutMapping public Film updateFilm(@Valid @RequestBody Film newFilm) { return filmService.updateFilm(newFilm); } + + @PutMapping("/{id}/like/{userId}") + public SuccessResponse likeFilm(@PathVariable Long id, @PathVariable Long userId) { + filmService.likeFilm(id, userId); + return new SuccessResponse(); + } + + @DeleteMapping("/{id}/like/{userId}") + public SuccessResponse removeLikeFilm(@PathVariable Long id, @PathVariable Long userId) { + filmService.removeLikeFilm(id, userId); + return new SuccessResponse(); + } + + @GetMapping("/popular") + public Collection getPopularFilms(@RequestParam(defaultValue = "10") int count) { + return filmService.getPopularFilms(count); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java index 91af745..847c3c5 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -2,8 +2,10 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.response.SuccessResponse; import ru.yandex.practicum.filmorate.service.UserService; import java.util.Collection; @@ -17,16 +19,48 @@ public class UserController { @GetMapping public Collection getUsers() { - return userService.getUsers(); + return userService.getAllUsers(); + } + + @GetMapping("/{userId}") + public User getFilm(@PathVariable Long userId) { + return userService.getUserById(userId); } @PostMapping + @ResponseStatus(HttpStatus.CREATED) public User createUser(@Valid @RequestBody User user) { - return userService.addUser(user); + return userService.createUser(user); } @PutMapping public User updateUser(@Valid @RequestBody User newUser) { return userService.updateUser(newUser); } + + @PutMapping("/{id}/friends/{friendId}") + public SuccessResponse addFriend(@PathVariable Long id, @PathVariable Long friendId) { + userService.addFriend(id, friendId); + return new SuccessResponse(); + } + + @DeleteMapping("/{id}/friends/{friendId}") + public SuccessResponse removeFriend(@PathVariable Long id, @PathVariable Long friendId) { + userService.removeFriend(id, friendId); + return new SuccessResponse(); + } + + @GetMapping("/{id}/friends") + public Collection getFriends(@PathVariable Long id) { + return userService.getUserFriends(id); + } + + @GetMapping("/{id}/friends/common/{otherId}") + public Collection getCommonFriends(@PathVariable Long id, @PathVariable Long otherId) { + return userService.getCommonUserFriends(id, otherId); + } + + + + } diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/NotFoundException.java b/src/main/java/ru/yandex/practicum/filmorate/exception/NotFoundException.java index dd8d262..417433b 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/exception/NotFoundException.java +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/NotFoundException.java @@ -1,6 +1,12 @@ package ru.yandex.practicum.filmorate.exception; +import lombok.Getter; + + +@Getter public class NotFoundException extends RuntimeException { + public String name = "not found"; + public NotFoundException(String message) { super(message); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/OtherException.java b/src/main/java/ru/yandex/practicum/filmorate/exception/OtherException.java new file mode 100644 index 0000000..e9694c1 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/OtherException.java @@ -0,0 +1,12 @@ +package ru.yandex.practicum.filmorate.exception; + +import lombok.Getter; + +@Getter +public class OtherException extends RuntimeException { + String name = "server error"; + + public OtherException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/ValidationException.java b/src/main/java/ru/yandex/practicum/filmorate/exception/ValidationException.java index 52dc49c..d578df8 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/exception/ValidationException.java +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ValidationException.java @@ -1,6 +1,11 @@ package ru.yandex.practicum.filmorate.exception; +import lombok.Getter; + +@Getter public class ValidationException extends RuntimeException { + String name = "validation error"; + public ValidationException(String message) { super(message); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/handler/ErrorHandler.java b/src/main/java/ru/yandex/practicum/filmorate/handler/ErrorHandler.java new file mode 100644 index 0000000..85b72a5 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/handler/ErrorHandler.java @@ -0,0 +1,52 @@ +package ru.yandex.practicum.filmorate.handler; + + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.validation.FieldError; +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.OtherException; +import ru.yandex.practicum.filmorate.exception.ValidationException; +import ru.yandex.practicum.filmorate.response.ErrorResponse; + +import java.util.List; + +@Slf4j +@RestControllerAdvice +public class ErrorHandler { + + @ExceptionHandler + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorResponse handleNotFound(final NotFoundException e) { + return new ErrorResponse(e.getName(), e.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleValidationException(MethodArgumentNotValidException ex) { + List errors = ex.getBindingResult().getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .toList(); + String message = String.join("\n", errors); + log.warn("Ошибка валидации: {}", errors); + + return new ErrorResponse("validation error", message); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleValidationError(final ValidationException e) { + return new ErrorResponse(e.getName(), e.getMessage()); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorResponse handleInternalServerError(final OtherException e) { + return new ErrorResponse(e.getName(), e.getMessage()); + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/handler/GlobalExceptionHandler.java b/src/main/java/ru/yandex/practicum/filmorate/handler/GlobalExceptionHandler.java deleted file mode 100644 index 306285b..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/handler/GlobalExceptionHandler.java +++ /dev/null @@ -1,25 +0,0 @@ -package ru.yandex.practicum.filmorate.handler; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import ru.yandex.practicum.filmorate.exception.ValidationException; - -import java.util.List; - -@Slf4j -@RestControllerAdvice -public class GlobalExceptionHandler { - - @ExceptionHandler(MethodArgumentNotValidException.class) - public void handleValidationException(MethodArgumentNotValidException ex) { - List errors = ex.getBindingResult().getFieldErrors().stream() - .map(FieldError::getDefaultMessage) - .toList(); - String message = String.join("\n", errors); - log.warn("Ошибка валидации: {}", errors); - throw new ValidationException(message); - } -} 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 6283263..660004b 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -2,19 +2,22 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.validation.constraints.*; -import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; /** * Film. */ @Data -@AllArgsConstructor +@NoArgsConstructor public class Film { private Long id; + private Set usersLikes = new HashSet<>(); @NotBlank(message = "Название фильма не может быть пустым") private String name; @@ -27,6 +30,14 @@ public class Film { @Positive(message = "Продолжительность фильма должна быть положительным числом") private Integer duration; + public Film(Long id, String name, String description, LocalDate releaseDate, Integer duration) { + this.id = id; + this.name = name; + this.description = description; + this.releaseDate = releaseDate; + this.duration = duration; + } + @AssertTrue(message = "Дата релиза фильма должна быть не раньше 28 декабря 1895 года") @JsonIgnore public boolean isReleaseDateValid() { diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/User.java b/src/main/java/ru/yandex/practicum/filmorate/model/User.java index 9eee6fb..95c2eda 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/User.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java @@ -4,19 +4,22 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.PastOrPresent; import jakarta.validation.constraints.Pattern; -import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; /** * User. */ @Data -@AllArgsConstructor +@NoArgsConstructor public class User { private Long id; + private Set friends = new HashSet<>(); @Email(message = "Электронная почта пользователя должна соответствовать формату электронного адреса") @NotBlank(message = "Электронная почта пользователя не может быть пустой") @@ -29,4 +32,12 @@ public class User { @PastOrPresent(message = "Дата рождения пользователя не может быть в будущем") private LocalDate birthday; + + public User(Long id, String email, String login, String name, LocalDate birthday) { + this.id = id; + this.email = email; + this.login = login; + this.name = name; + this.birthday = birthday; + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/response/ErrorResponse.java b/src/main/java/ru/yandex/practicum/filmorate/response/ErrorResponse.java new file mode 100644 index 0000000..1707772 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/response/ErrorResponse.java @@ -0,0 +1,12 @@ +package ru.yandex.practicum.filmorate.response; + + +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@RequiredArgsConstructor +public class ErrorResponse { + private final String error; + private final String message; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/response/SuccessResponse.java b/src/main/java/ru/yandex/practicum/filmorate/response/SuccessResponse.java new file mode 100644 index 0000000..be025ad --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/response/SuccessResponse.java @@ -0,0 +1,10 @@ +package ru.yandex.practicum.filmorate.response; + +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@RequiredArgsConstructor +public class SuccessResponse { + private final String status = "success"; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java index 983a303..6cfb0e6 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -1,34 +1,38 @@ package ru.yandex.practicum.filmorate.service; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.OtherException; import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.storage.film.FilmStorage; import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; +import java.util.Comparator; @Service @Slf4j +@RequiredArgsConstructor public class FilmService { - private final Map films = new HashMap<>(); + private final FilmStorage filmStorage; + private final UserService userService; + private final Comparator filmComparator = Comparator.comparing((Film film) -> film.getUsersLikes().size()).reversed(); - public Collection getFilms() { - return films.values(); + + public Collection getAllFilms() { + return filmStorage.getFilms(); } - public Map getFilmsMap() { - return films; + public Film getFilmById(Long id) { + return filmStorage.getFilm(id).orElseThrow(() -> new NotFoundException("Фильм с id " + id + " не найден")); } - public Film addFilm(Film film) { - film.setId(getNextId()); - films.put(film.getId(), film); + public Film createFilm(Film film) { log.info("Добавлен фильм: {}", film); - return film; + return filmStorage.addFilm(film); + } public Film updateFilm(Film newFilm) { @@ -36,30 +40,39 @@ public Film updateFilm(Film newFilm) { log.warn("В запросе на обновление фильма не передан id"); throw new ValidationException("Id не может быть пустым"); } - if (films.containsKey(newFilm.getId())) { - Film oldFilm = films.get(newFilm.getId()); - Optional.ofNullable(newFilm.getName()).ifPresent(oldFilm::setName); - Optional.ofNullable(newFilm.getDescription()).ifPresent(oldFilm::setDescription); - Optional.ofNullable(newFilm.getReleaseDate()).ifPresent(oldFilm::setReleaseDate); - Optional.ofNullable(newFilm.getDuration()).ifPresent(oldFilm::setDuration); - log.info("Обновлен фильм: {}", oldFilm); - return oldFilm; - } else { - log.warn("В запросе на обновление фильма передан неизвестный id - {}", newFilm.getId()); - throw new NotFoundException("Фильм с id " + newFilm.getId() + " не найден"); + + filmStorage.getFilm(newFilm.getId()) + .orElseThrow(() -> { + log.warn("В запросе на обновление фильма передан неизвестный id - {}", newFilm.getId()); + return new NotFoundException("Фильм с id " + newFilm.getId() + " не найден"); + }); + + return filmStorage.updateFilm(newFilm); + } + + public void likeFilm(Long filmId, Long userId) { + if (!getFilmById(filmId).getUsersLikes().add(userService.getUserById(userId).getId())) { + log.warn("Пользователь с id {} уже ставил лайк фильму с id {}", userId, filmId); + throw new OtherException("Пользователь уже ставил лайк фильму"); } + + log.info("Пользователь с id {} поставил лайк фильму с id {}", userId, filmId); + } + + public void removeLikeFilm(Long filmId, Long userId) { + if (!getFilmById(filmId).getUsersLikes().remove(userService.getUserById(userId).getId())) { + log.warn("Пользователь с id {} не ставил лайк фильму id {}", userId, filmId); + throw new OtherException("Пользователь не ставил лайк фильму"); + } + + log.info("Пользователь с id {} удалил свой лайк у фильма с id {}", userId, filmId); } - public void clearData() { - films.clear(); + public Collection getPopularFilms(int count) { + return filmStorage.getFilms().stream().sorted(filmComparator).limit(count).toList(); } - private long getNextId() { - long currentMaxId = films.keySet() - .stream() - .mapToLong(id -> id) - .max() - .orElse(0); - return ++currentMaxId; + public void clearFilmsData() { + filmStorage.clearData(); } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java index 4c204ab..5d38853 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -1,39 +1,38 @@ package ru.yandex.practicum.filmorate.service; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.OtherException; 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.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; @Service @Slf4j +@RequiredArgsConstructor public class UserService { - private final Map users = new HashMap<>(); + private final UserStorage userStorage; - public Collection getUsers() { - return users.values(); + public Collection getAllUsers() { + return userStorage.getUsers(); } - public Map getUsersMap() { - return users; + public User getUserById(Long id) { + return userStorage.getUser(id).orElseThrow(() -> new NotFoundException("Юзер с id " + id + " не найден")); } - public User addUser(User user) { - user.setId(getNextId()); + public User createUser(User user) { if (user.getName() == null || user.getName().isBlank()) { user.setName(user.getLogin()); log.info("В запросе создания пользователя передан пустой name. Для поля name использован логин {}", user.getLogin()); } - users.put(user.getId(), user); - log.info("Добавлен пользователь: {}", user); - return user; + + return userStorage.addUser(user); } public User updateUser(User newUser) { @@ -41,36 +40,64 @@ public User updateUser(User newUser) { log.warn("В запросе на обновление юзера не передан id"); throw new ValidationException("Id не может быть пустым"); } - if (users.containsKey(newUser.getId())) { - User oldUser = users.get(newUser.getId()); - if (newUser.getName() == null || newUser.getName().isBlank()) { - oldUser.setName(newUser.getLogin()); - log.info("В запросе обновления пользователя передан пустой name. Для поля name использован логин {}", - newUser.getLogin()); - } else { - Optional.of(newUser.getName()).ifPresent(oldUser::setName); - } - Optional.ofNullable(newUser.getEmail()).ifPresent(oldUser::setEmail); - Optional.ofNullable(newUser.getBirthday()).ifPresent(oldUser::setBirthday); - Optional.ofNullable(newUser.getLogin()).ifPresent(oldUser::setLogin); - log.info("Обновлен юзер: {}", oldUser); - return oldUser; - } else { - log.warn("В запросе на обновление пользователя передан неизвестный id - {}", newUser.getId()); - throw new NotFoundException("Пользователь с id " + newUser.getId() + " не найден"); + + userStorage.getUser(newUser.getId()) + .orElseThrow(() -> { + log.warn("В запросе на обновление пользователя передан неизвестный id - {}", newUser.getId()); + return new NotFoundException("Пользователь с id " + newUser.getId() + " не найден"); + }); + + if (newUser.getName() == null || newUser.getName().isBlank()) { + newUser.setName(newUser.getLogin()); + log.info("В запросе обновления пользователя передан пустой name. Для поля name использован логин {}", + newUser.getLogin()); + } + + return userStorage.updateUser(newUser); + + + } + + public void addFriend(Long id, Long friendId) { + User user = getUserById(id); + User friend = getUserById(friendId); + + if (id.equals(friendId)) { + log.warn("Пользователь не может добавить сам себя в друзья"); + throw new OtherException("Пользователь не может добавить сам себя в друзья"); + } + + if (!user.getFriends().add(friendId) || !friend.getFriends().add(id)) { + log.warn("Пользователи с id {} и id {} уже являются друзьями", id, friendId); + throw new OtherException("Пользователи уже являются друзьями"); + } + + log.info("Пользователь с id {} добавил в друзья пользователя с id {}", id, friendId); + } + + public void removeFriend(Long id, Long friendId) { + User user = getUserById(id); + User friend = getUserById(friendId); + + if (!user.getFriends().remove(friendId) || !friend.getFriends().remove(id)) { + log.warn("У пользователя с id {} не найден друг с id {}", id, friendId); } + + log.info("Пользователь с id {} удалил из друзей пользователя с id {}", id, friendId); + } + + public Collection getUserFriends(Long id) { + return getUserById(id).getFriends().stream() + .map(this::getUserById).toList(); } - public void clearData() { - users.clear(); + public Collection getCommonUserFriends(Long id, Long otherId) { + return getUserById(id).getFriends().stream() + .filter(getUserById(otherId).getFriends()::contains) + .map(this::getUserById).toList(); } - private long getNextId() { - long currentMaxId = users.keySet() - .stream() - .mapToLong(id -> id) - .max() - .orElse(0); - return ++currentMaxId; + public void clearUsersData() { + userStorage.clearData(); } } 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..1880bcb --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java @@ -0,0 +1,21 @@ +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 addFilm(Film film); + + Film updateFilm(Film film); + + Collection getFilms(); + + Optional getFilm(Long id); + + void clearData(); + + +} 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..a91d713 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java @@ -0,0 +1,58 @@ +package ru.yandex.practicum.filmorate.storage.film; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import ru.yandex.practicum.filmorate.model.Film; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Component +@Slf4j +public class InMemoryFilmStorage implements FilmStorage { + + private final Map films = new HashMap<>(); + + public Collection getFilms() { + return films.values(); + } + + public Optional getFilm(Long id) { + return films.values().stream() + .filter(film -> film.getId().equals(id)) + .findFirst(); + } + + public Film addFilm(Film film) { + film.setId(getNextId()); + films.put(film.getId(), film); + log.info("Добавлен фильм: {}", film); + return film; + } + + public Film updateFilm(Film newFilm) { + Film oldFilm = films.get(newFilm.getId()); + Optional.ofNullable(newFilm.getName()).ifPresent(oldFilm::setName); + Optional.ofNullable(newFilm.getDescription()).ifPresent(oldFilm::setDescription); + Optional.ofNullable(newFilm.getReleaseDate()).ifPresent(oldFilm::setReleaseDate); + Optional.ofNullable(newFilm.getDuration()).ifPresent(oldFilm::setDuration); + log.info("Обновлен фильм: {}", newFilm); + return oldFilm; + } + + private long getNextId() { + long currentMaxId = films.keySet() + .stream() + .mapToLong(id -> id) + .max() + .orElse(0); + return ++currentMaxId; + } + + public void clearData() { + films.clear(); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java new file mode 100644 index 0000000..76fae6a --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java @@ -0,0 +1,57 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.User; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Component +@Slf4j +public class InMemoryUserStorage implements UserStorage { + + private final Map users = new HashMap<>(); + + public Collection getUsers() { + return users.values(); + } + + public Optional getUser(Long id) { + return users.values().stream() + .filter(user -> user.getId().equals(id)) + .findFirst(); + } + + public User addUser(User user) { + user.setId(getNextId()); + users.put(user.getId(), user); + log.info("Добавлен пользователь: {}", user); + return user; + } + + public User updateUser(User newUser) { + User oldUser = users.get(newUser.getId()); + Optional.of(newUser.getName()).ifPresent(oldUser::setName); + Optional.ofNullable(newUser.getEmail()).ifPresent(oldUser::setEmail); + Optional.ofNullable(newUser.getBirthday()).ifPresent(oldUser::setBirthday); + Optional.ofNullable(newUser.getLogin()).ifPresent(oldUser::setLogin); + log.info("Обновлен юзер: {}", newUser); + return oldUser; + } + + public void clearData() { + users.clear(); + } + + private long getNextId() { + long currentMaxId = users.keySet() + .stream() + .mapToLong(id -> id) + .max() + .orElse(0); + return ++currentMaxId; + } +} 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..11dd5fa --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import ru.yandex.practicum.filmorate.model.User; + +import java.util.Collection; +import java.util.Optional; + +public interface UserStorage { + + User addUser(User user); + + User updateUser(User user); + + Collection getUsers(); + + Optional getUser(Long id); + + void clearData(); +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4b49e8e..10f7117 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,2 @@ -logging.level.root=INFO \ No newline at end of file +logging.level.root=INFO +logging.level.org.zalando.logbook= TRACE \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/FilmControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/FilmControllerTest.java index 87bf814..1b93f1c 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/FilmControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/FilmControllerTest.java @@ -1,38 +1,54 @@ package ru.yandex.practicum.filmorate; -import com.google.gson.JsonElement; -import com.google.gson.JsonParser; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; 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.boot.test.web.client.TestRestTemplate; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; +import org.springframework.test.web.servlet.MockMvc; import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.service.FilmService; +import ru.yandex.practicum.filmorate.service.UserService; import java.time.LocalDate; -import java.util.Objects; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@SpringBootTest +@AutoConfigureMockMvc class FilmControllerTest { @Autowired - private TestRestTemplate restTemplate; + private MockMvc mockMvc; @Autowired private FilmService filmService; - private final HttpHeaders headers = new HttpHeaders(); + @Autowired + private UserService userService; + + @BeforeEach + void setUp() { + filmService.createFilm(new Film(1L, "test1", "test_descr1", LocalDate.of(1900, 12, 25), 10)); + filmService.createFilm(new Film(2L, "test2", "test_descr2", LocalDate.of(1900, 12, 25), 10)); + userService.createUser(new User(1L, "test@mail.ru", "testlogin1", "testname1", LocalDate.of(1900, 12, 25))); + userService.createUser(new User(2L, "test2@mail.ru", "testlogin2", "testname2", LocalDate.of(1901, 10, 21))); + } + + @AfterEach + void tearDown() { + filmService.clearFilmsData(); + userService.clearUsersData(); + } static Stream provideInvalidFilmJsonCreate() { return Stream.of( @@ -123,173 +139,296 @@ static Stream provideInvalidFilmJsonUpdate() { " \"description\": \"Sci-fi action\",\n" + " \"releaseDate\": \"1900-12-25\",\n" + " \"duration\": -5\n" + - "}" // Отрицательная длительность - ); - } + "}", // Отрицательная длительность - static Stream provideInvalidFilmJsonIdUpdate() { - return Stream.of( "{\n" + + " \"id\": 135,\n" + " \"name\": \"The Matrix\",\n" + " \"description\": \"Sci-fi action\",\n" + " \"releaseDate\": \"1900-12-25\",\n" + - " \"duration\": 10\n" + - "}", // Без id - - "{\n" + - " \"id\": 125,\n" + - " \"name\": \"The Matrix\",\n" + - " \"description\": \"Sci-fi action\",\n" + - " \"releaseDate\": \"1900-12-25\",\n" + - " \"duration\": 10\n" + - "}" // Id нет в списке + " \"duration\": -5\n" + + "}" // неизвестный ид ); } @Test - void contextLoads() { + void getFilms() throws Exception { + mockMvc.perform(get("/films")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[0].name").value("test1")) + .andExpect(jsonPath("$[1].id").value(2)) + .andExpect(jsonPath("$[1].name").value("test2")); } - @BeforeEach - void setUp() { - headers.set("Content-Type", "application/json"); - filmService.clearData(); + @Test + void getFilmById() throws Exception { + mockMvc.perform(get("/films/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("test1")); } @Test - void getFilms() { - filmService.addFilm(new Film(1L, "The Matrix", "Sci-fi action", - LocalDate.of(1900, 12, 25), 10)); - filmService.addFilm(new Film(2L, "The Dark Knight", "Superhero thriller", - LocalDate.of(1900, 12, 25), 10)); - ResponseEntity response = restTemplate.getForEntity("/films", String.class); - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - JsonElement expectedJson = JsonParser.parseString( - "[\n" + - " {\n" + - " \"id\": 1,\n" + - " \"name\": \"The Matrix\",\n" + - " \"description\": \"Sci-fi action\",\n" + - " \"releaseDate\": \"1900-12-25\",\n" + - " \"duration\": 10\n" + - " },\n" + - " {\n" + - " \"id\": 2,\n" + - " \"name\": \"The Dark Knight\",\n" + - " \"description\": \"Superhero thriller\",\n" + - " \"releaseDate\": \"1900-12-25\",\n" + - " \"duration\": 10\n" + - " }\n" + - "]" - ); - assertEquals(expectedJson, JsonParser.parseString(response.getBody())); + void getUnknownFilmById() throws Exception { + mockMvc.perform(get("/films/5")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.error").value("not found")) + .andExpect(jsonPath("$.message").value("Фильм с id 5 не найден")); } @Test - void addFilm() { + void addFilms() throws Exception { String json = "{\n" + " \"name\": \"The Matrix\",\n" + " \"description\": \"Sci-fi action\",\n" + " \"releaseDate\": \"1967-03-25\",\n" + " \"duration\": 100\n" + "}"; - HttpEntity entity = new HttpEntity<>(json, headers); - ResponseEntity response = restTemplate.exchange("/films", HttpMethod.POST, entity, String.class); - assertEquals(HttpStatus.OK, response.getStatusCode()); + mockMvc.perform(post("/films") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(3)) + .andExpect(jsonPath("$.name").value("The Matrix")); - assertNotNull(response.getBody()); - JsonElement expectedJson = JsonParser.parseString( - "{\n" + - " \"id\": 1,\n" + - " \"name\": \"The Matrix\",\n" + - " \"description\": \"Sci-fi action\",\n" + - " \"releaseDate\": \"1967-03-25\",\n" + - " \"duration\": 100\n" + - "}" - ); - Film createdFilm = filmService.getFilmsMap().get(1L); - assertNotNull(createdFilm); - assertEquals(expectedJson, JsonParser.parseString(response.getBody())); - assertEquals(1L, createdFilm.getId()); - assertEquals("The Matrix", createdFilm.getName()); - assertEquals("Sci-fi action", createdFilm.getDescription()); - assertEquals(LocalDate.of(1967, 3, 25), createdFilm.getReleaseDate()); - assertEquals(100, createdFilm.getDuration()); + assertEquals(filmService.getFilmById(3L).getName(), "The Matrix"); } @Test - void updateFilm() { - filmService.addFilm(new Film(1L, "The Matrix", "Sci-fi action", - LocalDate.of(1900, 12, 25), 10)); + void updateFilms() throws Exception { String json = "{\n" + " \"id\": 1,\n" + - " \"name\": \"The Matrix updated\",\n" + - " \"description\": \"Sci-fi action updated\",\n" + + " \"name\": \"test1_upd\",\n" + + " \"description\": \"test_descr1_upd\",\n" + " \"releaseDate\": \"1967-03-25\",\n" + " \"duration\": 100\n" + "}"; - HttpEntity entity = new HttpEntity<>(json, headers); - ResponseEntity response = restTemplate.exchange("/films", HttpMethod.PUT, entity, String.class); - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - JsonElement expectedJson = JsonParser.parseString(json); - assertEquals(expectedJson, JsonParser.parseString(response.getBody())); - Film updatedFilm = filmService.getFilmsMap().get(1L); - assertNotNull(updatedFilm); - assertEquals(1L, updatedFilm.getId()); - assertEquals("The Matrix updated", updatedFilm.getName()); - assertEquals("Sci-fi action updated", updatedFilm.getDescription()); - assertEquals(LocalDate.of(1967, 3, 25), updatedFilm.getReleaseDate()); - assertEquals(100, updatedFilm.getDuration()); + + mockMvc.perform(put("/films") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("test1_upd")); + + assertEquals("test1_upd", filmService.getFilmById(1L).getName()); + assertEquals("test_descr1_upd", filmService.getFilmById(1L).getDescription()); + assertEquals("1967-03-25", filmService.getFilmById(1L).getReleaseDate().toString()); + assertEquals(100, filmService.getFilmById(1L).getDuration()); } @ParameterizedTest @MethodSource("provideInvalidFilmJsonCreate") - void addFilmValidation(String json) { - HttpEntity entity = new HttpEntity<>(json, headers); - ResponseEntity response = restTemplate.exchange("/films", HttpMethod.POST, entity, String.class); - assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - assertTrue(Objects.requireNonNull(response.getBody()).contains("\"error\":\"Bad Request\"")); + void addFilmValidation(String json) throws Exception { + mockMvc.perform(post("/films") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value("validation error")); } @Test - void addFilmEmptyJson() { - HttpEntity entity = new HttpEntity<>(null, headers); - ResponseEntity response = restTemplate.exchange("/films", HttpMethod.POST, entity, String.class); - assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - assertTrue(Objects.requireNonNull(response.getBody()).contains("\"error\":\"Bad Request\"")); + void addFilmEmptyJson() throws Exception { + String json = "{}"; + + mockMvc.perform(post("/films") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value("validation error")); } @ParameterizedTest @MethodSource("provideInvalidFilmJsonUpdate") - void updateFilmValidation(String json) { - filmService.addFilm(new Film(1L, "The Matrix", "Sci-fi action", - LocalDate.of(1900, 12, 25), 10)); - HttpEntity entity = new HttpEntity<>(json, headers); - ResponseEntity response = restTemplate.exchange("/films", HttpMethod.PUT, entity, String.class); - assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - assertTrue(Objects.requireNonNull(response.getBody()).contains("\"error\":\"Bad Request\"")); + void updateFilmValidation(String json) throws Exception { + mockMvc.perform(put("/films") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value("validation error")); } - @ParameterizedTest - @MethodSource("provideInvalidFilmJsonIdUpdate") - void updateFilmIdValidation(String json) { - filmService.addFilm(new Film(1L, "The Matrix", "Sci-fi action", - LocalDate.of(1900, 12, 25), 10)); - HttpEntity entity = new HttpEntity<>(json, headers); - ResponseEntity response = restTemplate.exchange("/films", HttpMethod.PUT, entity, String.class); - assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); - assertTrue(Objects.requireNonNull(response.getBody()).contains("\"error\":\"Internal Server Error\"")); + @Test + void updateFilmWithoutId() throws Exception { + String json = "{\n" + + " \"name\": \"The Matrix\",\n" + + " \"description\": \"Sci-fi action\",\n" + + " \"releaseDate\": \"1900-12-25\",\n" + + " \"duration\": 10\n" + + "}"; + + mockMvc.perform(put("/films") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value("validation error")); } @Test - void updateFilmEmptyJson() { - filmService.addFilm(new Film(1L, "The Matrix", "Sci-fi action", - LocalDate.of(1900, 12, 25), 10)); - HttpEntity entity = new HttpEntity<>(null, headers); - ResponseEntity response = restTemplate.exchange("/films", HttpMethod.PUT, entity, String.class); - assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - assertTrue(Objects.requireNonNull(response.getBody()).contains("\"error\":\"Bad Request\"")); + void updateFilmEmptyJson() throws Exception { + String json = "{}"; + + mockMvc.perform(put("/films") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value("validation error")); + } + + @Test + void likeFilm() throws Exception { + + mockMvc.perform(put("/films/1/like/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("success")); + + mockMvc.perform(put("/films/1/like/2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("success")); + + assertEquals(2, filmService.getFilmById(1L).getUsersLikes().size()); + } + + @Test + void deleteFilm() throws Exception { + filmService.likeFilm(1L, 1L); + filmService.likeFilm(1L, 2L); + mockMvc.perform(delete("/films/1/like/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("success")); + + mockMvc.perform(delete("/films/1/like/2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("success")); + + assertEquals(0, filmService.getFilmById(1L).getUsersLikes().size()); + } + + @Test + void likeUnknownFilm() throws Exception { + mockMvc.perform(put("/films/4/like/1")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.error").value("not found")) + .andExpect(jsonPath("$.message").value("Фильм с id 4 не найден")); + } + + @Test + void likeFilmUnkownUser() throws Exception { + mockMvc.perform(put("/films/1/like/4")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.error").value("not found")) + .andExpect(jsonPath("$.message").value("Юзер с id 4 не найден")); + } + + @Test + void deleteLikeFilm() throws Exception { + filmService.likeFilm(1L, 1L); + mockMvc.perform(delete("/films/1/like/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("success")); + + assertEquals(0, filmService.getFilmById(1L).getUsersLikes().size()); + } + + @Test + void deleteUnknownLikeFilm() throws Exception { + mockMvc.perform(delete("/films/1/like/1")) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.error").value("server error")) + .andExpect(jsonPath("$.message").value("Пользователь не ставил лайк фильму")); } + + @Test + void deleteLikeFilmUnkownUser() throws Exception { + mockMvc.perform(delete("/films/1/like/4")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.error").value("not found")) + .andExpect(jsonPath("$.message").value("Юзер с id 4 не найден")); + } + + @Test + void deleteLikeFilmUnkownFilm() throws Exception { + mockMvc.perform(delete("/films/4/like/1")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.error").value("not found")) + .andExpect(jsonPath("$.message").value("Фильм с id 4 не найден")); + } + + @Test + void get3PopularFilms() throws Exception { + filmService.createFilm(new Film(3L, "test3", "test_descr3", LocalDate.of(1900, 12, 25), 10)); + filmService.createFilm(new Film(4L, "test4", "test_descr4", LocalDate.of(1900, 12, 25), 10)); + userService.createUser(new User(3L, "test3@mail.ru", "testlogin3", "testname1", LocalDate.of(1900, 12, 25))); + userService.createUser(new User(4L, "test4@mail.ru", "testlogin4", "testname2", LocalDate.of(1901, 10, 21))); + filmService.createFilm(new Film(5L, "test5", "test_descr5", LocalDate.of(1900, 12, 25), 10)); + filmService.createFilm(new Film(6L, "test6", "test_descr6", LocalDate.of(1900, 12, 25), 10)); + userService.createUser(new User(5L, "test5@mail.ru", "testlogin5", "testname1", LocalDate.of(1900, 12, 25))); + userService.createUser(new User(6L, "test6@mail.ru", "testlogin6 ", "testname2", LocalDate.of(1901, 10, 21))); + filmService.likeFilm(3L, 1L); + filmService.likeFilm(3L, 2L); + filmService.likeFilm(3L, 3L); + filmService.likeFilm(3L, 4L); + filmService.likeFilm(1L, 5L); + filmService.likeFilm(1L, 6L); + filmService.likeFilm(4L, 6L); + filmService.likeFilm(2L, 1L); + filmService.likeFilm(2L, 2L); + mockMvc.perform(get("/films/popular?count=3")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()").value(3)) + .andExpect(jsonPath("$[0].id").value(3)) + .andExpect(jsonPath("$[1].name").value("test1")) + .andExpect(jsonPath("$[2].description").value("test_descr2")); + } + + @Test + void getDefaultPopularFilms() throws Exception { + filmService.createFilm(new Film(3L, "test3", "test_descr3", LocalDate.of(1900, 12, 25), 10)); + filmService.createFilm(new Film(4L, "test4", "test_descr4", LocalDate.of(1900, 12, 25), 10)); + userService.createUser(new User(3L, "test3@mail.ru", "testlogin3", "testname1", LocalDate.of(1900, 12, 25))); + userService.createUser(new User(4L, "test4@mail.ru", "testlogin4", "testname2", LocalDate.of(1901, 10, 21))); + filmService.createFilm(new Film(5L, "test5", "test_descr5", LocalDate.of(1900, 12, 25), 10)); + filmService.createFilm(new Film(6L, "test6", "test_descr6", LocalDate.of(1900, 12, 25), 10)); + userService.createUser(new User(5L, "test5@mail.ru", "testlogin5", "testname1", LocalDate.of(1900, 12, 25))); + userService.createUser(new User(6L, "test6@mail.ru", "testlogin6 ", "testname2", LocalDate.of(1901, 10, 21))); + filmService.createFilm(new Film(7L, "test7", "test_descr7", LocalDate.of(1900, 12, 25), 10)); + filmService.createFilm(new Film(8L, "test8", "test_descr8", LocalDate.of(1900, 12, 25), 10)); + userService.createUser(new User(7L, "test7@mail.ru", "testlogin7", "testname1", LocalDate.of(1900, 12, 25))); + userService.createUser(new User(8L, "test8@mail.ru", "testlogin8", "testname2", LocalDate.of(1901, 10, 21))); + filmService.createFilm(new Film(9L, "test9", "test_descr9", LocalDate.of(1900, 12, 25), 10)); + filmService.createFilm(new Film(10L, "test10", "test_descr10", LocalDate.of(1900, 12, 25), 10)); + userService.createUser(new User(9L, "test9@mail.ru", "testlogin9", "testname1", LocalDate.of(1900, 12, 25))); + userService.createUser(new User(10L, "test10@mail.ru", "testlogin10 ", "testname2", LocalDate.of(1901, 10, 21))); + filmService.createFilm(new Film(11L, "test11", "test_descr11", LocalDate.of(1900, 12, 25), 10)); + filmService.createFilm(new Film(12L, "test12", "test_descr12", LocalDate.of(1900, 12, 25), 10)); + userService.createUser(new User(11L, "test11@mail.ru", "testlogin11", "testname1", LocalDate.of(1900, 12, 25))); + userService.createUser(new User(12L, "test12@mail.ru", "testlogin12", "testname2", LocalDate.of(1901, 10, 21))); + filmService.createFilm(new Film(13L, "test13", "test_descr13", LocalDate.of(1900, 12, 25), 10)); + filmService.createFilm(new Film(14L, "test14", "test_descr14", LocalDate.of(1900, 12, 25), 10)); + userService.createUser(new User(13L, "test13@mail.ru", "testlogin13", "testname1", LocalDate.of(1900, 12, 25))); + userService.createUser(new User(14L, "test14@mail.ru", "testlogin14 ", "testname2", LocalDate.of(1901, 10, 21))); + filmService.likeFilm(3L, 1L); + filmService.likeFilm(3L, 2L); + filmService.likeFilm(3L, 3L); + filmService.likeFilm(3L, 4L); + filmService.likeFilm(1L, 5L); + filmService.likeFilm(1L, 6L); + filmService.likeFilm(4L, 6L); + filmService.likeFilm(5L, 1L); + filmService.likeFilm(5L, 2L); + filmService.likeFilm(6L, 1L); + filmService.likeFilm(7L, 2L); + filmService.likeFilm(8L, 3L); + filmService.likeFilm(6L, 4L); + filmService.likeFilm(10L, 5L); + filmService.likeFilm(11L, 6L); + filmService.likeFilm(12L, 6L); + filmService.likeFilm(10L, 1L); + filmService.likeFilm(10L, 2L); + mockMvc.perform(get("/films/popular")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()").value(10)); + } + } diff --git a/src/test/java/ru/yandex/practicum/filmorate/UserControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/UserControllerTest.java index 58319d5..6518009 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/UserControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/UserControllerTest.java @@ -1,45 +1,41 @@ package ru.yandex.practicum.filmorate; -import com.google.gson.JsonElement; -import com.google.gson.JsonParser; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; 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.boot.test.web.client.TestRestTemplate; -import org.springframework.http.*; + +import static org.junit.jupiter.api.Assertions.*; + +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; import ru.yandex.practicum.filmorate.model.User; + import ru.yandex.practicum.filmorate.service.UserService; import java.time.LocalDate; -import java.util.Objects; +import java.util.Arrays; import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@SpringBootTest +@AutoConfigureMockMvc public class UserControllerTest { @Autowired - private TestRestTemplate restTemplate; + private MockMvc mockMvc; @Autowired private UserService userService; - private final HttpHeaders headers = new HttpHeaders(); - - @Test - void contextLoads() { - } - - @BeforeEach - void setUp() { - headers.set("Content-Type", "application/json"); - userService.clearData(); - } - static Stream provideInvalidUserJsonCreate() { return Stream.of( "{\n" + @@ -123,213 +119,289 @@ static Stream provideInvalidUserJsonUpdate() { ); } - static Stream provideInvalidUserJsonIdUpdate() { - return Stream.of( - "{\n" + - " \"login\": \"dolore\",\n" + - " \"name\": \"Nick Name\",\n" + - " \"email\": \"mail@mail.ru\",\n" + - " \"birthday\": \"1946-08-20\"\n" + - "}", // Без Id + @BeforeEach + void setUp() { + userService.createUser(new User(1L, "test@mail.ru", "testlogin1", "testname1", LocalDate.of(1900, 12, 25))); + userService.createUser(new User(2L, "test2@mail.ru", "testlogin2", "testname2", LocalDate.of(1901, 10, 21))); + userService.createUser(new User(3L, "test3@mail.ru", "testlogin3", "testname3", LocalDate.of(1900, 12, 25))); + userService.createUser(new User(4L, "test4@mail.ru", "testlogin4", "testname4", LocalDate.of(1901, 10, 21))); + } - "{\n" + - " \"id\": 123,\n" + - " \"login\": \"dolore\",\n" + - " \"name\": \"Nick Name\",\n" + - " \"email\": \"mail@mail.ru\",\n" + - " \"birthday\": \"1946-08-20\"\n" + - "}" // Id отсутствует в списке - ); + @AfterEach + void tearDown() { + userService.clearUsersData(); } @Test - void getUsers() { - userService.addUser(new User(1L, "test@mail.ru", "login1", "user1", - LocalDate.of(1999, 2, 18))); - userService.addUser(new User(2L, "test2@mail.ru", "login2", "user2", - LocalDate.of(2001, 5, 25))); - - ResponseEntity response = restTemplate.getForEntity("/users", String.class); - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - JsonElement expectedJson = JsonParser.parseString( - "[\n" + - " {\n" + - " \"id\": 1,\n" + - " \"email\": \"test@mail.ru\",\n" + - " \"login\": \"login1\",\n" + - " \"name\": \"user1\",\n" + - " \"birthday\": \"1999-02-18\"\n" + - " },\n" + - " {\n" + - " \"id\": 2,\n" + - " \"email\": \"test2@mail.ru\",\n" + - " \"login\": \"login2\",\n" + - " \"name\": \"user2\",\n" + - " \"birthday\": \"2001-05-25\"\n" + - " }\n" + - "]" - ); - assertEquals(expectedJson, JsonParser.parseString(response.getBody())); + void getAllUsers() throws Exception { + mockMvc.perform(get("/users")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[1].name").value("testname2")) + .andExpect(jsonPath("$[2].email").value("test3@mail.ru")) + .andExpect(jsonPath("$[3].login").value("testlogin4")); + } + + @Test + void getUserById() throws Exception { + mockMvc.perform(get("/users/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("testname1")); } @Test - void addUser() { + void getUnknownUserById() throws Exception { + mockMvc.perform(get("/users/200")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.error").value("not found")) + .andExpect(jsonPath("$.message").value("Юзер с id 200 не найден")); + } + + @Test + void addUser() throws Exception { String json = "{\n" + " \"login\": \"testlogin\",\n" + " \"name\": \"Test User\",\n" + " \"email\": \"mail@mail.ru\",\n" + " \"birthday\": \"1946-08-20\"\n" + "}"; - HttpEntity entity = new HttpEntity<>(json, headers); - ResponseEntity response = restTemplate.exchange("/users", HttpMethod.POST, entity, String.class); - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - JsonElement expectedJson = JsonParser.parseString( - "{ \"id\": 1,\n" + - " \"login\": \"testlogin\",\n" + - " \"name\": \"Test User\",\n" + - " \"email\": \"mail@mail.ru\",\n" + - " \"birthday\": \"1946-08-20\"\n" + - "}" - ); - assertEquals(expectedJson, JsonParser.parseString(response.getBody())); - - User createdUser = userService.getUsersMap().get(1L); - assertNotNull(createdUser); - assertEquals("testlogin", createdUser.getLogin()); - assertEquals("Test User", createdUser.getName()); - assertEquals("mail@mail.ru", createdUser.getEmail()); - assertEquals(LocalDate.of(1946, 8, 20), createdUser.getBirthday()); + mockMvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(5)) + .andExpect(jsonPath("$.name").value("Test User")); + + assertEquals(userService.getUserById(5L).getLogin(), "testlogin"); } @Test - void addUserWithoutName() { + void addUserWithoutName() throws Exception { String json = "{\n" + " \"login\": \"testlogin\",\n" + " \"email\": \"mail@mail.ru\",\n" + " \"birthday\": \"1946-08-20\"\n" + "}"; - HttpEntity entity = new HttpEntity<>(json, headers); - ResponseEntity response = restTemplate.exchange("/users", HttpMethod.POST, entity, String.class); - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - - JsonElement expectedJson = JsonParser.parseString( - "{ \"id\": 1,\n" + - " \"login\": \"testlogin\",\n" + - " \"name\": \"testlogin\",\n" + - " \"email\": \"mail@mail.ru\",\n" + - " \"birthday\": \"1946-08-20\"\n" + - "}" - ); - assertEquals(expectedJson, JsonParser.parseString(response.getBody())); - - User createdUser = userService.getUsersMap().get(1L); - assertNotNull(createdUser); - assertEquals("testlogin", createdUser.getLogin()); - assertEquals("testlogin", createdUser.getName()); - assertEquals("mail@mail.ru", createdUser.getEmail()); - assertEquals(LocalDate.of(1946, 8, 20), createdUser.getBirthday()); + + mockMvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(5)) + .andExpect(jsonPath("$.name").value("testlogin")); + assertEquals(userService.getUserById(5L).getName(), "testlogin"); } @Test - void updateUser() { - userService.addUser(new User(1L, "test@mail.ru", "login1", "user1", - LocalDate.of(1999, 2, 18))); + void updateUser() throws Exception { String json = "{\n" + " \"id\": 1,\n" + - " \"login\": \"login1Upd\",\n" + - " \"name\": \"Test User upd\",\n" + - " \"email\": \"mailupd@mail.ru\",\n" + + " \"login\": \"testlogin1upd\",\n" + + " \"name\": \"testname1 upd\",\n" + + " \"email\": \"testupd@mail.ru\",\n" + " \"birthday\": \"1978-10-21\"\n" + "}"; - HttpEntity entity = new HttpEntity<>(json, headers); - ResponseEntity response = restTemplate.exchange("/users", HttpMethod.PUT, entity, String.class); - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - - JsonElement expectedJson = JsonParser.parseString(json); - assertEquals(expectedJson, JsonParser.parseString(response.getBody())); - - User updatedUser = userService.getUsersMap().get(1L); - assertNotNull(updatedUser); - assertEquals("login1Upd", updatedUser.getLogin()); - assertEquals("Test User upd", updatedUser.getName()); - assertEquals("mailupd@mail.ru", updatedUser.getEmail()); - assertEquals(LocalDate.of(1978, 10, 21), updatedUser.getBirthday()); + + mockMvc.perform(put("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("testname1 upd")) + .andExpect(jsonPath("$.email").value("testupd@mail.ru")) + .andExpect(jsonPath("$.login").value("testlogin1upd")) + .andExpect(jsonPath("$.birthday").value("1978-10-21")); + assertEquals("testlogin1upd", userService.getUserById(1L).getLogin()); + assertEquals("testname1 upd", userService.getUserById(1L).getName()); + assertEquals("testupd@mail.ru", userService.getUserById(1L).getEmail()); + assertEquals("1978-10-21", userService.getUserById(1L).getBirthday().toString()); } @Test - void updateUserWithoutName() { - userService.addUser(new User(1L, "test@mail.ru", "login1", "user1", - LocalDate.of(1999, 2, 18))); + void updateUserWithoutName() throws Exception { String json = "{\n" + " \"id\": 1,\n" + - " \"login\": \"login1Upd\",\n" + - " \"email\": \"mailupd@mail.ru\",\n" + + " \"login\": \"testlogin1upd\",\n" + + " \"email\": \"testupd@mail.ru\",\n" + " \"birthday\": \"1978-10-21\"\n" + "}"; - HttpEntity entity = new HttpEntity<>(json, headers); - ResponseEntity response = restTemplate.exchange("/users", HttpMethod.PUT, entity, String.class); - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - - User updatedUser = userService.getUsersMap().get(1L); - assertNotNull(updatedUser); - assertEquals("login1Upd", updatedUser.getLogin()); - assertEquals("login1Upd", updatedUser.getName()); - assertEquals("mailupd@mail.ru", updatedUser.getEmail()); - assertEquals(LocalDate.of(1978, 10, 21), updatedUser.getBirthday()); + + mockMvc.perform(put("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("testlogin1upd")) + .andExpect(jsonPath("$.email").value("testupd@mail.ru")) + .andExpect(jsonPath("$.login").value("testlogin1upd")) + .andExpect(jsonPath("$.birthday").value("1978-10-21")); + assertEquals("testlogin1upd", userService.getUserById(1L).getLogin()); + assertEquals("testlogin1upd", userService.getUserById(1L).getName()); + assertEquals("testupd@mail.ru", userService.getUserById(1L).getEmail()); + assertEquals("1978-10-21", userService.getUserById(1L).getBirthday().toString()); + } + + @ParameterizedTest + @MethodSource("provideInvalidUserJsonUpdate") + void updateUserValidation(String json) throws Exception { + mockMvc.perform(put("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value("validation error")); + } @ParameterizedTest @MethodSource("provideInvalidUserJsonCreate") - void addUserValidation(String json) { - HttpEntity entity = new HttpEntity<>(json, headers); - ResponseEntity response = restTemplate.exchange("/users", HttpMethod.POST, entity, String.class); - assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - assertTrue(Objects.requireNonNull(response.getBody()).contains("\"error\":\"Bad Request\"")); + void addUserValidation(String json) throws Exception { + mockMvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value("validation error")); + } @Test - void addUserEmptyJson() { - HttpEntity entity = new HttpEntity<>(null, headers); - ResponseEntity response = restTemplate.exchange("/users", HttpMethod.POST, entity, String.class); - assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - assertTrue(Objects.requireNonNull(response.getBody()).contains("\"error\":\"Bad Request\"")); + void addUserEmptyJson() throws Exception { + String json = "{}"; + + mockMvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value("validation error")); } - @ParameterizedTest - @MethodSource("provideInvalidUserJsonUpdate") - void updateUserValidation(String json) { - userService.addUser(new User(1L, "test@mail.ru", "login1", "user1", - LocalDate.of(1999, 2, 18))); - HttpEntity entity = new HttpEntity<>(json, headers); - ResponseEntity response = restTemplate.exchange("/users", HttpMethod.PUT, entity, String.class); - assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - assertTrue(Objects.requireNonNull(response.getBody()).contains("\"error\":\"Bad Request\"")); + @Test + void updateUserWithoutId() throws Exception { + String json = "{\n" + + " \"login\": \"dolore\",\n" + + " \"name\": \"Nick Name\",\n" + + " \"email\": \"mail@mail.ru\",\n" + + " \"birthday\": \"1946-08-20\"\n" + + "}"; + + mockMvc.perform(put("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value("validation error")); } - @ParameterizedTest - @MethodSource("provideInvalidUserJsonIdUpdate") - void updateUserIdValidation(String json) { - userService.addUser(new User(1L, "test@mail.ru", "login1", "user1", - LocalDate.of(1999, 2, 18))); - HttpEntity entity = new HttpEntity<>(json, headers); - ResponseEntity response = restTemplate.exchange("/users", HttpMethod.PUT, entity, String.class); - assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); - assertTrue(Objects.requireNonNull(response.getBody()).contains("\"error\":\"Internal Server Error\"")); + @Test + void updateUnknownUser() throws Exception { + String json = "{\n" + + " \"id\": 123,\n" + + " \"login\": \"dolore\",\n" + + " \"name\": \"Nick Name\",\n" + + " \"email\": \"mail@mail.ru\",\n" + + " \"birthday\": \"1946-08-20\"\n" + + "}"; + + mockMvc.perform(put("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.error").value("not found")) + .andExpect(jsonPath("$.message").value("Пользователь с id 123 не найден")); + } + + @Test + void updateUserEmptyJson() throws Exception { + String json = "{}"; + + mockMvc.perform(put("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value("validation error")); } @Test - void updateUserEmptyJson() { - userService.addUser(new User(1L, "test@mail.ru", "login1", "user1", - LocalDate.of(1999, 2, 18))); - HttpEntity entity = new HttpEntity<>(null, headers); - ResponseEntity response = restTemplate.exchange("/users", HttpMethod.PUT, entity, String.class); - assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - assertTrue(Objects.requireNonNull(response.getBody()).contains("\"error\":\"Bad Request\"")); + void addFriend() throws Exception { + mockMvc.perform(put("/users/1/friends/2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("success")); + + mockMvc.perform(put("/users/1/friends/3")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("success")); + + assertEquals(2, userService.getUserById(1L).getFriends().size()); + assertEquals(1, userService.getUserById(2L).getFriends().size()); + assertEquals(1, userService.getUserById(3L).getFriends().size()); + assertEquals(Arrays.toString(new int[]{2, 3}), userService.getUserById(1L).getFriends().toString()); } + @Test + void removeFriend() throws Exception { + userService.addFriend(1L, 2L); + userService.addFriend(1L, 3L); + + mockMvc.perform(delete("/users/1/friends/2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("success")); + + mockMvc.perform(delete("/users/1/friends/3")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("success")); + + assertEquals(0, userService.getUserById(1L).getFriends().size()); + assertEquals(0, userService.getUserById(2L).getFriends().size()); + assertEquals(0, userService.getUserById(3L).getFriends().size()); + } + + @Test + void addUnknownFriend() throws Exception { + mockMvc.perform(put("/users/1/friends/10")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value("Юзер с id 10 не найден")); + } + + @Test + void addFriendMyself() throws Exception { + mockMvc.perform(put("/users/1/friends/1")) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.message").value("Пользователь не может " + + "добавить сам себя в друзья")); + } + + @Test + void addFriendUserFriend() throws Exception { + userService.addFriend(1L, 2L); + + mockMvc.perform(put("/users/1/friends/2")) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.message").value("Пользователи уже являются друзьями")); + } + + @Test + void getUserFriends() throws Exception { + userService.addFriend(1L, 2L); + userService.addFriend(1L, 3L); + + mockMvc.perform(get("/users/1/friends")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()").value(2)) + .andExpect(jsonPath("$[0].id").value(2)) + .andExpect(jsonPath("$[1].name").value("testname3")); + } + + @Test + void getCommonUserFriends() throws Exception { + userService.addFriend(1L, 2L); + userService.addFriend(1L, 3L); + userService.addFriend(4L, 2L); + userService.addFriend(4L, 3L); + mockMvc.perform(get("/users/1/friends/common/4")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()").value(2)) + .andExpect(jsonPath("$[0].id").value(2)) + .andExpect(jsonPath("$[1].name").value("testname3")); + } + + }