diff --git a/README.md b/README.md index 8b13789..bd3c7cc 100644 --- a/README.md +++ b/README.md @@ -1 +1,73 @@ + +## Схема БД (ER-диаграмма) + +![ER-диаграмма для Filmorate](filmorate_database.png) + +## Описание таблиц + +Основу данных составляют таблицы `user` и `film`, которые хранят информацию о пользователях и фильмах соответственно. + +Для хранения стандартизированных значений, таких как жанры и возрастные рейтинги MPA, были созданы отдельные таблицы-справочники: `genre` и `mpa_rating`. Такой подход позволяет избежать дублирования данных и упрощает их обновление. + +Связи "многие-ко-многим" реализуются через промежуточные таблицы. Таблица `film_genre` связывает фильмы с их жанрами. `film_like` отслеживает, какие пользователи поставили лайки фильмам, что необходимо для расчета популярности. Таблица `friendship` реализует механику дружбы между пользователями, включая статус подтверждения (`is_confirmed`). + +## Примеры SQL-запросов + +### Получение всех фильмов с их жанрами и рейтингом MPA + +```sql +SELECT + f.id, + f.name, + f.description, + f.release_date, + f.duration, + mr.name AS mpa_rating, + GROUP_CONCAT(g.name SEPARATOR ', ') AS genres +FROM + film AS f +JOIN + mpa_rating AS mr ON f.mpa_rating_id = mr.id +LEFT JOIN + film_genre AS fg ON f.id = fg.film_id +LEFT JOIN + genre AS g ON fg.genre_id = g.id +GROUP BY + f.id; +``` + +### Запрос на получение 10 самых популярных фильмов + +```sql +SELECT + f.id, + f.name, + COUNT(fl.user_id) AS likes_count +FROM + film AS f +LEFT JOIN + film_like AS fl ON f.id = fl.film_id +GROUP BY + f.id +ORDER BY + likes_count DESC +LIMIT 10; +``` + +### Поиск общих друзей для двух пользователей (id 1 и 2) + +```sql +SELECT + u.* +FROM + friendship AS f1 +JOIN + friendship AS f2 ON f1.friend_id = f2.friend_id +JOIN + user AS u ON f1.friend_id = u.id +WHERE + f1.user_id = 1 AND f2.user_id = 2 + AND f1.is_confirmed = true AND f2.is_confirmed = true; +``` + diff --git a/filmorate_database.png b/filmorate_database.png new file mode 100644 index 0000000..364b4cf Binary files /dev/null and b/filmorate_database.png differ diff --git a/pom.xml b/pom.xml index 0cad031..cca16ab 100644 --- a/pom.xml +++ b/pom.xml @@ -1,32 +1,53 @@ - + 4.0.0 + org.springframework.boot spring-boot-starter-parent - 3.2.4 - + 3.2.0 + + ru.yandex.practicum filmorate - 0.0.1-SNAPSHOT - filmorate - filmorate + 1.0.0 + - 21 + 17 + 3.9.0 + + org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-validation + + + + + org.zalando + logbook-spring-boot-starter + ${logbook.version} + + + org.projectlombok lombok provided + + org.springframework.boot spring-boot-starter-test @@ -40,7 +61,26 @@ org.springframework.boot spring-boot-maven-plugin + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + ${lombok.version} + + + + - + + + central + https://repo.maven.apache.org/maven2 + + + \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java b/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java index dca451b..8f860a2 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java +++ b/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java @@ -8,5 +8,4 @@ public class FilmorateApplication { public static void main(String[] args) { SpringApplication.run(FilmorateApplication.class, args); } - -} +} \ No newline at end of file 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..e138bc5 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,57 @@ package ru.yandex.practicum.filmorate.controller; -import org.springframework.web.bind.annotation.RestController; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.service.FilmService; + +import java.util.List; @RestController +@RequestMapping("/films") +@Slf4j public class FilmController { -} + private final FilmService filmService; + + @Autowired + public FilmController(FilmService filmService) { + this.filmService = filmService; + } + + @GetMapping + public List getAllFilms() { + return filmService.getAll(); + } + + @GetMapping("/{id}") + public Film getFilmById(@PathVariable int id) { + return filmService.getById(id); + } + + @PostMapping + public Film createFilm(@Valid @RequestBody Film film) { + return filmService.create(film); + } + + @PutMapping + public Film updateFilm(@Valid @RequestBody Film film) { + return filmService.update(film); + } + + @PutMapping("/{id}/like/{userId}") + public void addLike(@PathVariable int id, @PathVariable int userId) { + filmService.addLike(id, userId); + } + + @DeleteMapping("/{id}/like/{userId}") + public void removeLike(@PathVariable int id, @PathVariable int userId) { + filmService.removeLike(id, userId); + } + + @GetMapping("/popular") + public List getPopularFilms(@RequestParam(defaultValue = "10") int count) { + return filmService.getPopularFilms(count); + } +} \ No newline at end of file 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..961e226 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -0,0 +1,62 @@ +package ru.yandex.practicum.filmorate.controller; + +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.service.UserService; + +import java.util.List; + +@RestController +@RequestMapping("/users") +@Slf4j +public class UserController { + private final UserService userService; + + @Autowired + public UserController(UserService userService) { + this.userService = userService; + } + + @GetMapping + public List getAllUsers() { + return userService.getAll(); + } + + @GetMapping("/{id}") + public User getUserById(@PathVariable int id) { + return userService.getById(id); + } + + @PostMapping + public User createUser(@Valid @RequestBody User user) { + return userService.create(user); + } + + @PutMapping + public User updateUser(@Valid @RequestBody User user) { + return userService.update(user); + } + + @PutMapping("/{id}/friends/{friendId}") + public void addFriend(@PathVariable int id, @PathVariable int friendId) { + userService.addFriend(id, friendId); + } + + @DeleteMapping("/{id}/friends/{friendId}") + public void removeFriend(@PathVariable int id, @PathVariable int friendId) { + userService.removeFriend(id, friendId); + } + + @GetMapping("/{id}/friends") + public List getFriends(@PathVariable int id) { + return userService.getFriends(id); + } + + @GetMapping("/{id}/friends/common/{otherId}") + public List getCommonFriends(@PathVariable int id, @PathVariable int otherId) { + return userService.getCommonFriends(id, otherId); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/error/ErrorHandler.java b/src/main/java/ru/yandex/practicum/filmorate/error/ErrorHandler.java new file mode 100644 index 0000000..dcf6964 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/error/ErrorHandler.java @@ -0,0 +1,45 @@ +package ru.yandex.practicum.filmorate.error; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ValidationException; + +@RestControllerAdvice +@Slf4j +public class ErrorHandler { + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleValidationException(final MethodArgumentNotValidException e) { + log.error("Ошибка валидации: {}", e.getMessage()); + return new ErrorResponse( + e.getBindingResult().getFieldError().getDefaultMessage() + ); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleCustomValidationException(final ValidationException e) { + log.error("Ошибка валидации: {}", e.getMessage()); + return new ErrorResponse(e.getMessage()); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorResponse handleNotFoundException(final NotFoundException e) { + log.error("Объект не найден: {}", e.getMessage()); + return new ErrorResponse(e.getMessage()); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorResponse handleThrowable(final Throwable e) { + log.error("Внутренняя ошибка сервера: ", e); + return new ErrorResponse("Произошла непредвиденная ошибка."); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/error/ErrorResponse.java b/src/main/java/ru/yandex/practicum/filmorate/error/ErrorResponse.java new file mode 100644 index 0000000..9aa31ba --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/error/ErrorResponse.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.error; + +public class ErrorResponse { + private final String error; + + public ErrorResponse(String error) { + this.error = error; + } + + public String getError() { + return error; + } +} \ No newline at end of file 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..82e3f72 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} \ No newline at end of file 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..532f9bd --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ValidationException.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.exception; + +public class ValidationException extends RuntimeException { + public ValidationException(String message) { + super(message); + } +} \ No newline at end of file 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..ecb293e 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,33 @@ package ru.yandex.practicum.filmorate.model; -import lombok.Getter; -import lombok.Setter; - -/** - * Film. - */ -@Getter -@Setter +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; +import lombok.Data; +import lombok.experimental.FieldDefaults; +import com.fasterxml.jackson.annotation.JsonIgnore; + +import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) public class Film { -} + int id; + + @NotBlank(message = "Название не может быть пустым.") + String name; + + @Size(max = 200, message = "Описание не должно превышать 200 символов.") + String description; + + LocalDate releaseDate; + + @Positive(message = "Продолжительность фильма должна быть положительной.") + int duration; + + @JsonIgnore + Set likes = new HashSet<>(); +} \ No newline at end of file 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..f360e27 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java @@ -0,0 +1,36 @@ +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.AccessLevel; +import lombok.Data; +import lombok.experimental.FieldDefaults; +import com.fasterxml.jackson.annotation.JsonIgnore; + +import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +public class User { + int id; + + @NotBlank(message = "Email не может быть пустым.") + @Email(message = "Email должен быть в правильном формате.") + String email; + + @NotBlank(message = "Логин не может быть пустым.") + @Pattern(regexp = "\\S*", message = "Логин не может содержать пробелы.") + String login; + + String name; + + @PastOrPresent(message = "Дата рождения не может быть в будущем.") + LocalDate birthday; + + @JsonIgnore + Set friends = new HashSet<>(); +} \ No newline at end of file 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..c10bd71 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -0,0 +1,93 @@ +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.Film; +import ru.yandex.practicum.filmorate.storage.FilmStorage; +import ru.yandex.practicum.filmorate.storage.UserStorage; + +import java.time.LocalDate; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class FilmService { + private static final LocalDate EARLIEST_RELEASE_DATE = LocalDate.of(1895, 12, 28); + + private final FilmStorage filmStorage; + private final UserStorage userStorage; + + @Autowired + public FilmService(FilmStorage filmStorage, UserStorage userStorage) { + this.filmStorage = filmStorage; + this.userStorage = userStorage; + } + + public Film create(Film film) { + validateFilm(film); + return filmStorage.create(film); + } + + public Film update(Film film) { + validateFilm(film); + return filmStorage.update(film); + } + + public Film getById(int id) { + return filmStorage.getById(id); + } + + public List getAll() { + return filmStorage.getAll(); + } + + public void addLike(int filmId, int userId) { + Film film = filmStorage.getById(filmId); + userStorage.getById(userId); // Проверяем существование пользователя + + film.getLikes().add(userId); + filmStorage.update(film); + + log.info("Пользователь с id={} поставил лайк фильму с id={}", userId, filmId); + } + + public void removeLike(int filmId, int userId) { + Film film = filmStorage.getById(filmId); + + if (!film.getLikes().contains(userId)) { + throw new NotFoundException("Лайк от пользователя с id=" + userId + " не найден."); + } + + film.getLikes().remove(userId); + filmStorage.update(film); + + log.info("Пользователь с id={} удалил лайк фильму с id={}", userId, filmId); + } + + public List getPopularFilms(int count) { + return filmStorage.getAll().stream() + .sorted(Comparator.comparingInt((Film f) -> f.getLikes().size()).reversed()) + .limit(count) + .collect(Collectors.toList()); + } + + private void validateFilm(Film film) { + if (film.getName() == null || film.getName().isBlank()) { + throw new ValidationException("Название фильма не может быть пустым."); + } + if (film.getDescription() != null && film.getDescription().length() > 200) { + throw new ValidationException("Описание не должно превышать 200 символов."); + } + if (film.getReleaseDate() == null || film.getReleaseDate().isBefore(EARLIEST_RELEASE_DATE)) { + throw new ValidationException("Дата релиза не может быть раньше 28 декабря 1895 года."); + } + if (film.getDuration() <= 0) { + throw new ValidationException("Продолжительность фильма должна быть положительной."); + } + } +} \ No newline at end of file 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..42f81ec --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -0,0 +1,97 @@ +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.model.User; +import ru.yandex.practicum.filmorate.storage.UserStorage; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Service +@Slf4j +public class UserService { + private final UserStorage userStorage; + + @Autowired + public UserService(UserStorage userStorage) { + this.userStorage = userStorage; + } + + public User create(User user) { + if (user.getName() == null || user.getName().isBlank()) { + user.setName(user.getLogin()); + } + return userStorage.create(user); + } + + public User update(User user) { + if (user.getName() == null || user.getName().isBlank()) { + user.setName(user.getLogin()); + } + return userStorage.update(user); + } + + public User getById(int id) { + return userStorage.getById(id); + } + + public List getAll() { + return userStorage.getAll(); + } + + public void addFriend(int userId, int friendId) { + User user = userStorage.getById(userId); + User friend = userStorage.getById(friendId); + + user.getFriends().add(friendId); + friend.getFriends().add(userId); + + userStorage.update(user); + userStorage.update(friend); + + log.info("Пользователь с id={} добавил в друзья пользователя с id={}", userId, friendId); + } + + public void removeFriend(int userId, int friendId) { + User user = userStorage.getById(userId); + User friend = userStorage.getById(friendId); + + user.getFriends().remove(friendId); + friend.getFriends().remove(userId); + + userStorage.update(user); + userStorage.update(friend); + + log.info("Пользователь с id={} удалил из друзей пользователя с id={}", userId, friendId); + } + + public List getFriends(int userId) { + User user = userStorage.getById(userId); + List friends = new ArrayList<>(); + + for (Integer friendId : user.getFriends()) { + friends.add(userStorage.getById(friendId)); + } + + return friends; + } + + public List getCommonFriends(int userId, int otherId) { + User user = userStorage.getById(userId); + User other = userStorage.getById(otherId); + + Set commonFriendIds = new HashSet<>(user.getFriends()); + commonFriendIds.retainAll(other.getFriends()); + + List commonFriends = new ArrayList<>(); + for (Integer friendId : commonFriendIds) { + commonFriends.add(userStorage.getById(friendId)); + } + + return commonFriends; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java new file mode 100644 index 0000000..8c4f56d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java @@ -0,0 +1,17 @@ +package ru.yandex.practicum.filmorate.storage; + +import ru.yandex.practicum.filmorate.model.Film; +import java.util.List; + +public interface FilmStorage { + + Film create(Film film); + + Film update(Film film); + + Film getById(int id); + + List getAll(); + + void delete(int id); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryFilmStorage.java new file mode 100644 index 0000000..61c6774 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryFilmStorage.java @@ -0,0 +1,58 @@ +package ru.yandex.practicum.filmorate.storage; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.storage.FilmStorage; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Component +@Slf4j +public class InMemoryFilmStorage implements FilmStorage { + private final Map films = new HashMap<>(); + private int nextId = 1; + + @Override + public Film create(Film film) { + film.setId(nextId++); + films.put(film.getId(), film); + log.info("Фильм '{}' с id={} успешно создан.", film.getName(), film.getId()); + return film; + } + + @Override + public Film update(Film film) { + if (!films.containsKey(film.getId())) { + throw new NotFoundException("Фильм с id=" + film.getId() + " не найден."); + } + films.put(film.getId(), film); + log.info("Фильм '{}' с id={} успешно обновлен.", film.getName(), film.getId()); + return film; + } + + @Override + public Film getById(int id) { + Film film = films.get(id); + if (film == null) { + throw new NotFoundException("Фильм с id=" + id + " не найден."); + } + return film; + } + + @Override + public List getAll() { + return new ArrayList<>(films.values()); + } + + @Override + public void delete(int id) { + if (!films.containsKey(id)) { + throw new NotFoundException("Фильм с id=" + id + " не найден."); + } + films.remove(id); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryUserStorage.java new file mode 100644 index 0000000..403d2c8 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryUserStorage.java @@ -0,0 +1,58 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.UserStorage; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Component +@Slf4j +public class InMemoryUserStorage implements UserStorage { + private final Map users = new HashMap<>(); + private int nextId = 1; + + @Override + public User create(User user) { + user.setId(nextId++); + users.put(user.getId(), user); + log.info("Пользователь '{}' с id={} успешно создан.", user.getLogin(), user.getId()); + return user; + } + + @Override + public User update(User user) { + if (!users.containsKey(user.getId())) { + throw new NotFoundException("Пользователь с id=" + user.getId() + " не найден."); + } + users.put(user.getId(), user); + log.info("Пользователь '{}' с id={} успешно обновлен.", user.getLogin(), user.getId()); + return user; + } + + @Override + public User getById(int id) { + User user = users.get(id); + if (user == null) { + throw new NotFoundException("Пользователь с id=" + id + " не найден."); + } + return user; + } + + @Override + public List getAll() { + return new ArrayList<>(users.values()); + } + + @Override + public void delete(int id) { + if (!users.containsKey(id)) { + throw new NotFoundException("Пользователь с id=" + id + " не найден."); + } + users.remove(id); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java new file mode 100644 index 0000000..b34fd80 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java @@ -0,0 +1,17 @@ +package ru.yandex.practicum.filmorate.storage; + +import ru.yandex.practicum.filmorate.model.User; +import java.util.List; + +public interface UserStorage { + + User create(User user); + + User update(User user); + + User getById(int id); + + List getAll(); + + void delete(int id); +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b13789..636895c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,2 @@ - +# Logbook configuration +logging.level.org.zalando.logbook=TRACE diff --git a/src/test/java/ru/yandex/practicum/filmorate/FilmControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/FilmControllerTest.java new file mode 100644 index 0000000..63c6d6e --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/FilmControllerTest.java @@ -0,0 +1,122 @@ +package ru.yandex.practicum.filmorate; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import ru.yandex.practicum.filmorate.controller.FilmController; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.service.FilmService; + +import java.time.LocalDate; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(FilmController.class) +class FilmControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private FilmService filmService; + + @Autowired + private ObjectMapper objectMapper; + + private Film validFilm; + + @BeforeEach + void setUp() { + validFilm = new Film(); + validFilm.setId(1); + validFilm.setName("Test Film"); + validFilm.setDescription("Test Description"); + validFilm.setReleaseDate(LocalDate.of(2000, 1, 1)); + validFilm.setDuration(120); + } + + @Test + void shouldCreateFilm() throws Exception { + when(filmService.create(any(Film.class))).thenReturn(validFilm); + + mockMvc.perform(post("/films") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validFilm))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("Test Film")) + .andExpect(jsonPath("$.duration").value(120)); + } + + @Test + void shouldGetAllFilms() throws Exception { + when(filmService.getAll()).thenReturn(List.of(validFilm)); + + mockMvc.perform(get("/films")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[0].name").value("Test Film")); + } + + @Test + void shouldGetFilmById() throws Exception { + when(filmService.getById(1)).thenReturn(validFilm); + + mockMvc.perform(get("/films/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("Test Film")); + } + + @Test + void shouldUpdateFilm() throws Exception { + when(filmService.update(any(Film.class))).thenReturn(validFilm); + + mockMvc.perform(put("/films") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validFilm))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("Test Film")); + } + + @Test + void shouldAddLike() throws Exception { + mockMvc.perform(put("/films/1/like/2")) + .andExpect(status().isOk()); + } + + @Test + void shouldRemoveLike() throws Exception { + mockMvc.perform(delete("/films/1/like/2")) + .andExpect(status().isOk()); + } + + @Test + void shouldGetPopularFilms() throws Exception { + when(filmService.getPopularFilms(anyInt())).thenReturn(List.of(validFilm)); + + mockMvc.perform(get("/films/popular?count=10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)); + } + + @Test + void shouldGetPopularFilmsWithDefaultCount() throws Exception { + when(filmService.getPopularFilms(10)).thenReturn(List.of(validFilm)); + + mockMvc.perform(get("/films/popular")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/UserControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/UserControllerTest.java new file mode 100644 index 0000000..1fd378d --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/UserControllerTest.java @@ -0,0 +1,122 @@ +package ru.yandex.practicum.filmorate; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import ru.yandex.practicum.filmorate.controller.UserController; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.service.UserService; + +import java.time.LocalDate; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(UserController.class) +class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private UserService userService; + + @Autowired + private ObjectMapper objectMapper; + + private User validUser; + + @BeforeEach + void setUp() { + validUser = new User(); + validUser.setId(1); + validUser.setEmail("test@example.com"); + validUser.setLogin("testLogin"); + validUser.setName("Test Name"); + validUser.setBirthday(LocalDate.of(1990, 1, 1)); + } + + @Test + void shouldCreateUser() throws Exception { + when(userService.create(any(User.class))).thenReturn(validUser); + + mockMvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validUser))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.email").value("test@example.com")) + .andExpect(jsonPath("$.login").value("testLogin")); + } + + @Test + void shouldGetAllUsers() throws Exception { + when(userService.getAll()).thenReturn(List.of(validUser)); + + mockMvc.perform(get("/users")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[0].email").value("test@example.com")); + } + + @Test + void shouldGetUserById() throws Exception { + when(userService.getById(1)).thenReturn(validUser); + + mockMvc.perform(get("/users/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.email").value("test@example.com")); + } + + @Test + void shouldUpdateUser() throws Exception { + when(userService.update(any(User.class))).thenReturn(validUser); + + mockMvc.perform(put("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validUser))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.email").value("test@example.com")); + } + + @Test + void shouldAddFriend() throws Exception { + mockMvc.perform(put("/users/1/friends/2")) + .andExpect(status().isOk()); + } + + @Test + void shouldRemoveFriend() throws Exception { + mockMvc.perform(delete("/users/1/friends/2")) + .andExpect(status().isOk()); + } + + @Test + void shouldGetFriends() throws Exception { + when(userService.getFriends(anyInt())).thenReturn(List.of(validUser)); + + mockMvc.perform(get("/users/1/friends")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)); + } + + @Test + void shouldGetCommonFriends() throws Exception { + when(userService.getCommonFriends(anyInt(), anyInt())).thenReturn(List.of(validUser)); + + mockMvc.perform(get("/users/1/friends/common/2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)); + } +} \ No newline at end of file