diff --git a/README.md b/README.md index 05a8e4d..493a848 100644 --- a/README.md +++ b/README.md @@ -10,22 +10,30 @@ ## Текущий этап работы над проектом: Спринт 12: - - Проектирование будущей базы данных; - - Создание ER-диаграммы базы данных. +- Переработана архитектура проекта; +- Добавлены новые модели: MPA-рейтинг, Genre, Likes, Friendship; +- Добавлен слой DAL для работы с данными из базы данных; +- Добавлен маппинг для преобразования записей из базы данных в объекты и обратно; +- Написаны интеграционные тесты; +- Добавлены тестовые хранилища для unit-тестов; +- Переработаны unit-тесты для граничных условий с учетом новой архитектуры проекта; ## Этапы работы над проектом 1. Спринт 10: - - Создание каркаса приложения с использользованием Spring Boot; - - Вся логика и хранение данных находятся в контроллерах; - - Разработан базовый функционал: добавление, обновление и получение всех фильмов/пользователей; - - Написаны unit-тесты для текущего функционала. + - Создание каркаса приложения с использользованием Spring Boot; + - Вся логика и хранение данных находятся в контроллерах; + - Разработан базовый функционал: добавление, обновление и получение всех фильмов/пользователей; + - Написаны unit-тесты для текущего функционала. 2. Спринт 11: - - Добавлены классы-хранилища для работы с данными о фильмах и пользователях; - - Добавлен новый функционал, позволяющий добавлять друзей и ставить лайки фильмам; - - Добавлены аннотации @service и @component для внедрения зависимостей; - - В классы контроллеры добавлена аннотация @ResponseStatus для указания HTTP-статуса ответа, который должен вернуться при обработке запроса; - - Добавлены классы ErrorHandler и ErrorResponse для централизованной обработки ошибок; - - Добавлены unit-тесты для нового функционала. + - Добавлены классы-хранилища для работы с данными о фильмах и пользователях; + - Добавлен новый функционал, позволяющий добавлять друзей и ставить лайки фильмам; + - Добавлены аннотации @service и @component для внедрения зависимостей; + - В классы контроллеры добавлена аннотация @ResponseStatus для указания HTTP-статуса ответа, который должен вернуться при обработке запроса; + - Добавлены классы ErrorHandler и ErrorResponse для централизованной обработки ошибок; + - Добавлены unit-тесты для нового функционала. +3. Спринт 12. Промежуточное задание: + - Спроектирована будущая база данных; + - Создана ER-диаграмма базы данных. ## ER-диаграмма ![Схема](filmorate_diagram.png) @@ -34,7 +42,7 @@ 1. Получить всех пользователей, кто родился не раньше 1995 года ```sql -SELECT * +SELECT u.user_id, u.email, u.login, u.name, u.birthday FROM users AS u WHERE EXTRACT(YEAR FROM birthday) >= 1995 @@ -43,20 +51,19 @@ ORDER BY birthday 2. Получить фильмы, у которых не меньше двух лайков ```sql SELECT f.film_id, - f.name AS film_name, + f.name AS title, f.description, f.release_date, + m.name AS mpa_rating, COUNT(l.user_id) AS likes_count FROM films AS f LEFT JOIN likes l ON f.film_id = l.film_id -GROUP BY f.film_id, - f.name, - f.description, - f.release_date +JOIN mpa_rating AS m ON f.rating_id = m.rating_id +GROUP BY f.film_id, m.name HAVING COUNT(l.user_id) >= 2 ORDER BY likes_count DESC; ``` - + 3. Получить пользователей, у которых яндекс почта ```sql SELECT u.user_id, @@ -64,5 +71,6 @@ SELECT u.user_id, u.login, u.name FROM users AS u -WHERE email LIKE '%yandex%' -``` +WHERE email LIKE '%yandex%' OR email LIKE '%@ya.ru%' +ORDER BY user_id +``` \ No newline at end of file diff --git a/filmorate_diagram.png b/filmorate_diagram.png index c3812f8..68b563d 100644 Binary files a/filmorate_diagram.png and b/filmorate_diagram.png differ diff --git a/pom.xml b/pom.xml index 9c95881..f1eb76c 100644 --- a/pom.xml +++ b/pom.xml @@ -1,59 +1,63 @@ - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.2.4 - - - ru.yandex.practicum - filmorate - 0.0.1-SNAPSHOT - filmorate - filmorate - - 21 - - - - org.springframework.boot - spring-boot-starter-web - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.4 + + + ru.yandex.practicum + filmorate + 0.0.1-SNAPSHOT + filmorate + filmorate + + 21 + + + + org.springframework.boot + spring-boot-starter-web + + + org.projectlombok + lombok + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-validation + + + org.zalando + logbook-spring-boot-starter + 3.7.2 + + + org.springframework.boot + spring-boot-starter-data-jdbc + + + com.h2database + h2 + runtime + + - - org.projectlombok - lombok - provided - + + + + org.springframework.boot + spring-boot-maven-plugin + + + - - org.springframework.boot - spring-boot-starter-test - test - - - - org.springframework.boot - spring-boot-starter-validation - - - - org.zalando - logbook-spring-boot-starter - 3.7.2 - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - + \ 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 7829e10..f47eae7 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -4,7 +4,9 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; -import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.dto.FilmDto; +import ru.yandex.practicum.filmorate.dto.NewFilmRequest; +import ru.yandex.practicum.filmorate.dto.UpdateFilmRequest; import ru.yandex.practicum.filmorate.service.FilmService; import java.util.Collection; @@ -18,22 +20,22 @@ public class FilmController { private final FilmService filmService; @GetMapping - public Collection findAll() { + public Collection findAll() { log.info("Получен запрос на получение списка всех фильмов"); return filmService.findAll(); } @PostMapping @ResponseStatus(HttpStatus.CREATED) - public Film create(@RequestBody Film newFilm) { - log.info("Получен запрос на добавление фильма {}", newFilm.getName()); - return filmService.create(newFilm); + public FilmDto create(@RequestBody NewFilmRequest newFilmRequest) { + log.info("Получен запрос на добавление фильма {}", newFilmRequest.getName()); + return filmService.create(newFilmRequest); } @PutMapping - public Film update(@RequestBody Film film) { - log.info("Получен запрос на обновление фильма c Id:{}", film.getId()); - return filmService.update(film); + public FilmDto update(@RequestBody UpdateFilmRequest filmRequest) { + log.info("Получен запрос на обновление фильма c Id:{}", filmRequest.getId()); + return filmService.update(filmRequest); } @DeleteMapping("/{id}") @@ -44,9 +46,9 @@ public void delete(@PathVariable("id") Long filmId) { } @GetMapping("/{id}") - public Film getById(@PathVariable("id") Long filmId) { + public FilmDto getById(@PathVariable("id") Long filmId) { log.info("Получен запрос на получение фильма с Id:{}", filmId); - return filmService.getById(filmId); + return filmService.findById(filmId); } @PutMapping("/{id}/like/{userId}") @@ -64,9 +66,9 @@ public void removeLike(@PathVariable("id") Long filmId, @PathVariable("userId") } @GetMapping("/popular") - public Collection getPopularFilms( + public Collection getPopularFilms( @RequestParam(defaultValue = "10") int count) { log.info("Получен запрос на получение топ-{} фильмов по количеству лайков", count); - return filmService.getPopularFilms(count); + return filmService.findPopularFilms(count); } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java new file mode 100644 index 0000000..c024332 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java @@ -0,0 +1,33 @@ +package ru.yandex.practicum.filmorate.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.service.GenreService; + +import java.util.Collection; + +@Slf4j +@RestController +@RequestMapping("/genres") +@RequiredArgsConstructor +public class GenreController { + + private final GenreService genreService; + + @GetMapping + public Collection findAll() { + log.info("Получен запрос на получение всех пользователей"); + return genreService.findAll(); + } + + @GetMapping("/{id}") + public Genre findById(@PathVariable("id") Long genreId) { + log.info("Получен запрос на получение жанра с id:{}", genreId); + return genreService.findById(genreId); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java new file mode 100644 index 0000000..4f148c0 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java @@ -0,0 +1,33 @@ +package ru.yandex.practicum.filmorate.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import ru.yandex.practicum.filmorate.model.Mpa; +import ru.yandex.practicum.filmorate.service.MpaService; + +import java.util.Collection; + +@Slf4j +@RestController +@RequestMapping("/mpa") +@RequiredArgsConstructor +public class MpaController { + + private final MpaService mpaService; + + @GetMapping + public Collection findAll() { + log.info("Получен запрос на получение всех MPA-рейтингов"); + return mpaService.findAll(); + } + + @GetMapping("/{id}") + public Mpa findById(@PathVariable("id") Long ratingId) { + log.info("Получен запрос на получение MPA-рейтинга с id:{}", ratingId); + return mpaService.findById(ratingId); + } +} \ 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 index e0f3ebe..80e262f 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -4,7 +4,9 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; -import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.dto.NewUserRequest; +import ru.yandex.practicum.filmorate.dto.UpdateUserRequest; +import ru.yandex.practicum.filmorate.dto.UserDto; import ru.yandex.practicum.filmorate.service.UserService; import java.util.*; @@ -18,22 +20,22 @@ public class UserController { private final UserService userService; @GetMapping - public Collection findAll() { + public Collection findAll() { log.info("Получен запрос на получение всех пользователей"); return userService.findAll(); } @PostMapping @ResponseStatus(HttpStatus.CREATED) - public User create(@RequestBody User newUser) { - log.info("Получен запрос на добавление нового пользователя {}", newUser.getLogin()); - return userService.create(newUser); + public UserDto create(@RequestBody NewUserRequest newUserRequest) { + log.info("Получен запрос на добавление нового пользователя {}", newUserRequest.getLogin()); + return userService.create(newUserRequest); } @PutMapping - public User update(@RequestBody User user) { - log.info("Получен запрос на обновление пользователя с id:{}", user.getId()); - return userService.update(user); + public UserDto update(@RequestBody UpdateUserRequest userRequest) { + log.info("Получен запрос на обновление пользователя с id:{}", userRequest.getId()); + return userService.update(userRequest); } @DeleteMapping("/{id}") @@ -44,7 +46,7 @@ public void delete(@PathVariable("id") Long userId) { } @GetMapping("/{id}") - public User getById(@PathVariable("id") Long userId) { + public UserDto getById(@PathVariable("id") Long userId) { log.info("Получен запрос на получение пользователя с Id:{}", userId); return userService.getById(userId); } @@ -64,14 +66,14 @@ public void removeFriend(@PathVariable("id") Long userId, @PathVariable("friendI } @GetMapping("/{id}/friends") - public Collection getFriends(@PathVariable("id") Long userId) { + public Collection getFriends(@PathVariable("id") Long userId) { log.info("Получен запрос на получение списка всех друзей пользователя с Id:{}", userId); - return userService.getFriends(userId); + return userService.findAllFriends(userId); } @GetMapping("/{id}/friends/common/{otherId}") - public Collection getCommonFriends(@PathVariable("id") Long userId, @PathVariable("otherId") Long otherId) { + public Collection getCommonFriends(@PathVariable("id") Long userId, @PathVariable("otherId") Long otherId) { log.info("Получен запрос на получение списка общих друзей у пользователей с Id:{} и {}", userId, otherId); - return userService.getCommonFriends(userId, otherId); + return userService.findCommonFriends(userId, otherId); } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/BaseDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/BaseDbStorage.java new file mode 100644 index 0000000..ba00dc4 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/BaseDbStorage.java @@ -0,0 +1,64 @@ +package ru.yandex.practicum.filmorate.dal; + +import lombok.RequiredArgsConstructor; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import ru.yandex.practicum.filmorate.exception.InternalServerException; + +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +public class BaseDbStorage { + + protected final JdbcTemplate jdbc; + protected final RowMapper mapper; + + protected Optional findOne(String query, Object... params) { + try { + T result = jdbc.queryForObject(query, mapper, params); + return Optional.ofNullable(result); + } catch (EmptyResultDataAccessException ignored) { + return Optional.empty(); + } + } + + protected List findMany(String query, Object... params) { + return jdbc.query(query, mapper, params); + } + + protected boolean delete(String query, long id) { + int rowsDeleted = jdbc.update(query, id); + return rowsDeleted > 0; + } + + protected void update(String query, Object... params) { + int rowsUpdated = jdbc.update(query, params); + if (rowsUpdated == 0) { + throw new InternalServerException("Не удалось обновить данные"); + } + } + + protected long insert(String query, Object... params) { + GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); + jdbc.update(connection -> { + PreparedStatement ps = connection + .prepareStatement(query, Statement.RETURN_GENERATED_KEYS); + for (int idx = 0; idx < params.length; idx++) { + ps.setObject(idx + 1, params[idx]); + } + return ps; }, keyHolder); + + Long id = keyHolder.getKeyAs(Long.class); + + if (id != null) { + return id; + } else { + throw new InternalServerException("Не удалось сохранить данные"); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmDbStorage.java new file mode 100644 index 0000000..811245b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmDbStorage.java @@ -0,0 +1,158 @@ +package ru.yandex.practicum.filmorate.dal; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.*; +import ru.yandex.practicum.filmorate.storage.*; + +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Repository +public class FilmDbStorage extends BaseDbStorage implements FilmStorage { + + @Autowired + private GenreStorage genreStorage; + @Autowired + private LikesStorage likesStorage; + @Autowired + private MpaStorage mpaStorage; + + private static final String INSERT_QUERY = "INSERT INTO films" + + "(name, description, release_date, duration, rating_id) " + + "VALUES (?, ?, ?, ?, ?)"; + private static final String DELETE_QUERY = "DELETE FROM films WHERE film_id = ?"; + private static final String UPDATE_QUERY = "UPDATE films SET " + + "name = ?, description = ?, release_date = ?, duration = ?, rating_id = ? WHERE film_id = ?"; + private static final String FIND_BY_ID_QUERY = "SELECT f.film_id, f.name, f.description, f.release_date, " + + "f.duration, m.rating_id, m.name AS mpa_name " + + "FROM films AS f " + + "INNER JOIN mpa_rating AS m ON f.rating_id = m.rating_id " + + "WHERE f.film_id = ?"; + private static final String FIND_ALL_FILMS_QUERY = "SELECT f.film_id, f.name, f.description, f.release_date, " + + "f.duration, m.rating_id, m.name AS mpa_name " + + "FROM films AS f " + + "INNER JOIN mpa_rating AS m ON f.rating_id = m.rating_id"; + private static final String FIND_POPULAR_FILMS_QUERY = "SELECT f.film_id, f.name, f.description, " + + "f.release_date, f.duration, m.rating_id, m.name AS mpa_name, COUNT(l.user_id) AS likes " + + "FROM films As f " + + "INNER JOIN mpa_rating AS m ON f.rating_id = m.rating_id " + + "LEFT JOIN likes AS l ON f.film_id = l.film_id " + + "GROUP BY f.film_id " + + "ORDER BY likes DESC " + + "LIMIT ?"; + + public FilmDbStorage(JdbcTemplate jdbc, RowMapper mapper) { + super(jdbc, mapper); + } + + // Добавление фильма + @Override + public Film create(Film film) { + long id = insert( + INSERT_QUERY, + film.getName(), + film.getDescription(), + film.getReleaseDate(), + film.getDuration(), + film.getMpa().getId() + ); + film.setId(id); + + if (film.getGenres() != null) { + genreStorage.setGenreForFilm(id, film.getGenres()); + } + + mpaStorage.findMpaFilm(id).ifPresent(film::setMpa); + + return film; + } + + // Удаление фильма + @Override + public void delete(Long id) { + delete(DELETE_QUERY, id); + } + + // Изменение фильма + @Override + public Film update(Film film) { + update( + UPDATE_QUERY, + film.getName(), + film.getDescription(), + film.getReleaseDate(), + film.getDuration(), + film.getMpa().getId(), + film.getId() + ); + + if (film.getGenres() != null) { + genreStorage.setGenreForFilm(film.getId(), film.getGenres()); + } + + // Возвращаем полный объект из БД + return findById(film.getId()).orElseThrow(); + } + + // Получение фильма по id + @Override + public Optional findById(Long id) { + Optional film = findOne(FIND_BY_ID_QUERY, id); + film.ifPresent(this::loadFilmData); + return film; + } + + // Получение всех фильмов + @Override + public Collection findAll() { + Collection films = findMany(FIND_ALL_FILMS_QUERY); + films.forEach(this::loadFilmData); + return films; + } + + // Получение популярных фильмов по количеству лайков + @Override + public Collection findPopularFilms(int count) { + Collection films = findMany(FIND_POPULAR_FILMS_QUERY, count); + films.forEach(this::loadFilmData); + return films; + } + + // Общий вспомогательный метод для загрузки всех данных фильма + private void loadFilmData(Film film) { + loadLikes(film); + loadGenres(film); + loadMpa(film); + } + + // Вспомогательный метод для загрузки данных о лайках фильма + private void loadLikes(Film film) { + Set likes = likesStorage.getLikesOnFilm(film.getId()).stream() + .map(Likes::getUserId) + .collect(Collectors.toCollection(HashSet::new)); + film.setLikesByUsers(likes); + } + + // Вспомогательный метод для загрузки данных о жанрах фильма + private void loadGenres(Film film) { + Collection genres = genreStorage.findGenreForFilm(film.getId()); + film.setGenres(genres != null ? + genres.stream() + .sorted(Comparator.comparing(Genre::getId)) + .collect(Collectors.toCollection(LinkedHashSet::new)) : + new LinkedHashSet<>()); + } + + // Вспомогательный метод для загрузки данных о рейтинге фильма + private void loadMpa(Film film) { + if (film.getMpa() == null || film.getMpa().getName() == null) { + mpaStorage.findMpaById(film.getMpa().getId()) + .ifPresent(film::setMpa); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/FriendshipDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/FriendshipDbStorage.java new file mode 100644 index 0000000..4cb5c01 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/FriendshipDbStorage.java @@ -0,0 +1,66 @@ +package ru.yandex.practicum.filmorate.dal; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.*; +import ru.yandex.practicum.filmorate.storage.FriendshipStorage; + +import java.util.Collection; +import java.util.Optional; +import java.util.stream.Collectors; + +@Slf4j +@Repository +public class FriendshipDbStorage extends BaseDbStorage implements FriendshipStorage { + + private final UserDbStorage userStorage; + + private static final String INSERT_QUERY = "INSERT INTO friendship(user_id, friend_id) VALUES (?, ?)"; + private static final String DELETE_QUERY = "DELETE FROM friendship WHERE user_id = ? AND friend_id = ?"; + private static final String FIND_FRIENDS_QUERY = + "SELECT f.user_id, f.friend_id FROM friendship f WHERE f.user_id = ?"; + private static final String FIND_COMMON_FRIENDS_QUERY = + "SELECT f1.user_id, f1.friend_id " + + "FROM friendship f1 " + + "JOIN friendship f2 ON f1.friend_id = f2.friend_id " + + "WHERE f1.user_id = ? AND f2.user_id = ?"; + + public FriendshipDbStorage(JdbcTemplate jdbc, RowMapper mapper, UserDbStorage userStorage) { + super(jdbc, mapper); + this.userStorage = userStorage; + } + + // Добавление друга + @Override + public void addFriend(Long userId, Long friendId) { + update(INSERT_QUERY, userId, friendId); + } + + // Удаление друга + @Override + public void removeFriend(Long userId, Long friendId) { + update(DELETE_QUERY, userId, friendId); + } + + // Получение всех друзей пользователя + @Override + public Collection findAllFriends(Long userId) { + return findMany(FIND_FRIENDS_QUERY, userId).stream() + .map(friendship -> userStorage.findById(friendship.getFriendId())) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + } + + // Получение общих друзей + @Override + public Collection findCommonFriends(Long userId, Long otherId) { + return findMany(FIND_COMMON_FRIENDS_QUERY, userId, otherId).stream() + .map(friendship -> userStorage.findById(friendship.getFriendId())) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/GenreDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/GenreDbStorage.java new file mode 100644 index 0000000..e7aac81 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/GenreDbStorage.java @@ -0,0 +1,64 @@ +package ru.yandex.practicum.filmorate.dal; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.storage.GenreStorage; + +import java.util.Collection; +import java.util.Optional; + +@Slf4j +@Repository +public class GenreDbStorage extends BaseDbStorage implements GenreStorage { + + private static final String FIND_ALL_GENRES_QUERY = "SELECT genre_id, name FROM genre"; + private static final String FIND_BY_ID_QUERY = "SELECT genre_id, name FROM genre WHERE genre_id = ?"; + private static final String FIND_GENRE_FOR_FILM_QUERY = "SELECT g.genre_id, g.name FROM genre As g " + + "INNER JOIN film_genres AS fg ON g.genre_id = fg.genre_id " + + "WHERE fg.film_id = ?"; + private static final String INSERT_GENRES_FOR_FILM_QUERY = "INSERT INTO film_genres (film_id, genre_id) " + + "VALUES(?, ?)"; + private static final String DELETE_ALL_GENRES_FOR_FILM_QUERY = "DELETE FROM film_genres WHERE film_id = ?"; + + public GenreDbStorage(JdbcTemplate jdbc, RowMapper mapper) { + super(jdbc, mapper); + } + + // Получение всех жанров + @Override + public Collection findAllGenres() { + return findMany(FIND_ALL_GENRES_QUERY); + } + + // Получение жанра по id + @Override + public Optional findById(Long genreId) { + return findOne(FIND_BY_ID_QUERY, genreId); + } + + // Получение жанров для указанного фильма + @Override + public Collection findGenreForFilm(Long filmId) { + return findMany(FIND_GENRE_FOR_FILM_QUERY, filmId); + } + + // Добавление жанра для фильма + @Override + public void setGenreForFilm(Long filmId, Collection genres) { + if (genres == null) { + return; + } + + // Удаление всех текущих жанров + jdbc.update(DELETE_ALL_GENRES_FOR_FILM_QUERY, filmId); + + // Добавление новых жанров + for (Genre genre : genres) { + jdbc.update(INSERT_GENRES_FOR_FILM_QUERY, + filmId, genre.getId()); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/LikesDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/LikesDbStorage.java new file mode 100644 index 0000000..98fd65b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/LikesDbStorage.java @@ -0,0 +1,41 @@ +package ru.yandex.practicum.filmorate.dal; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.Likes; +import ru.yandex.practicum.filmorate.storage.LikesStorage; + +import java.util.Collection; + +@Slf4j +@Repository +public class LikesDbStorage extends BaseDbStorage implements LikesStorage { + + private static final String INSERT_QUERY = "INSERT INTO likes (film_id, user_id) VALUES (?, ?)"; + private static final String DELETE_QUERY = "DELETE FROM likes WHERE film_id = ? AND user_id = ?"; + private static final String FIND_LIKES_ON_FILM_QUERY = "SELECT film_id, user_id FROM likes WHERE film_id = ?"; + + public LikesDbStorage(JdbcTemplate jdbc, RowMapper mapper) { + super(jdbc, mapper); + } + + // Добавление лайка фильму + @Override + public void addLikeToFilm(Long filmId, Long userId) { + update(INSERT_QUERY, filmId, userId); + } + + // Удаление лайка у фильма + @Override + public void removeLikeFromFilm(Long filmId, Long userId) { + update(DELETE_QUERY, filmId, userId); + } + + // Получение всех лайков фильма + @Override + public Collection getLikesOnFilm(Long filmId) { + return findMany(FIND_LIKES_ON_FILM_QUERY, filmId); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/MpaDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/MpaDbStorage.java new file mode 100644 index 0000000..0fd1046 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/MpaDbStorage.java @@ -0,0 +1,44 @@ +package ru.yandex.practicum.filmorate.dal; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.Mpa; +import ru.yandex.practicum.filmorate.storage.MpaStorage; + +import java.util.Collection; +import java.util.Optional; + +@Slf4j +@Repository +public class MpaDbStorage extends BaseDbStorage implements MpaStorage { + + private static final String FIND_ALL_MPA_QUERY = "SELECT rating_id, name FROM mpa_rating"; + private static final String FIND_BY_ID_QUERY = "SELECT rating_id, name FROM mpa_rating WHERE rating_id = ?"; + private static final String FIND_MPA_FILM_QUERY = "SELECT m.rating_id, m.name FROM films AS f " + + "INNER JOIN mpa_rating AS m ON f.rating_id = m.rating_id " + + "WHERE f.film_id = ?"; + + public MpaDbStorage(JdbcTemplate jdbc, RowMapper mapper) { + super(jdbc, mapper); + } + + // Получение всех рейтингов + @Override + public Collection findAll() { + return findMany(FIND_ALL_MPA_QUERY); + } + + // Получение рейтинга по его id + @Override + public Optional findMpaById(Long ratingId) { + return findOne(FIND_BY_ID_QUERY, ratingId); + } + + // Получение рейтинга для фильма + @Override + public Optional findMpaFilm(Long filmId) { + return findOne(FIND_MPA_FILM_QUERY, filmId); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/UserDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/UserDbStorage.java new file mode 100644 index 0000000..16e3e78 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/UserDbStorage.java @@ -0,0 +1,101 @@ +package ru.yandex.practicum.filmorate.dal; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.UserStorage; + +import java.util.Collection; +import java.util.Optional; +import java.util.stream.Collectors; + +@Slf4j +@Repository +public class UserDbStorage extends BaseDbStorage implements UserStorage { + + private static final String INSERT_QUERY = "INSERT INTO users(email, login, name, birthday) " + + "VALUES(?, ?, ?, ?)"; + private static final String DELETE_QUERY = "DELETE FROM users WHERE user_id = ?"; + private static final String UPDATE_QUERY = "UPDATE users SET email = ?, login = ?, name = ?, birthday = ? " + + "WHERE user_id = ?"; + private static final String FIND_BY_ID_QUERY = "SELECT user_id, email, login, name, birthday " + + "FROM users " + + "WHERE user_id = ?"; + private static final String FIND_ALL_USERS_QUERY = "SELECT user_id, email, login, name, birthday FROM users"; + private static final String FIND_FRIENDS_QUERY = "SELECT user_id, email, login, name, birthday FROM users " + + "WHERE user_id IN(SELECT friend_id FROM friendship WHERE user_id = ?)"; + + public UserDbStorage(JdbcTemplate jdbc, RowMapper mapper) { + super(jdbc, mapper); + } + + // Добавление пользователя + @Override + public User create(User user) { + long id = insert( + INSERT_QUERY, + user.getEmail(), + user.getLogin(), + user.getName(), + user.getBirthday() + ); + user.setId(id); + return user; + } + + // Удаление пользователя + @Override + public void delete(Long id) { + delete(DELETE_QUERY, id); + } + + // Изменение пользователя + @Override + public User update(User user) { + update( + UPDATE_QUERY, + user.getEmail(), + user.getLogin(), + user.getName(), + user.getBirthday(), + user.getId() + ); + return user; + } + + // Получение пользователя по id + @Override + public Optional findById(Long id) { + Optional user = findOne(FIND_BY_ID_QUERY, id); + user.ifPresent(value -> value.setFriends( + findMany(FIND_FRIENDS_QUERY, value.getId()) + .stream() + .map(User::getId) + .collect(Collectors.toSet()) + )); + return user; + } + + // Получение всех пользователей + @Override + public Collection findAll() { + Collection users = findMany(FIND_ALL_USERS_QUERY); + + if (users.isEmpty()) { + return users; + } + + users.forEach(user -> { + user.setFriends( + findMany(FIND_FRIENDS_QUERY, user.getId()) + .stream() + .map(User::getId) + .collect(Collectors.toSet()) + ); + }); + + return users; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/FilmRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/FilmRowMapper.java new file mode 100644 index 0000000..430d335 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/FilmRowMapper.java @@ -0,0 +1,35 @@ +package ru.yandex.practicum.filmorate.dal.mappers; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashSet; +import java.util.LinkedHashSet; + +@Component +public class FilmRowMapper implements RowMapper { + + @Override + public Film mapRow(ResultSet rs, int rowNum) throws SQLException { + Film film = new Film(); + film.setId(rs.getLong("film_id")); + film.setName(rs.getString("name")); + film.setDescription(rs.getString("description")); + film.setReleaseDate(rs.getDate("release_date").toLocalDate()); + film.setDuration(rs.getInt("duration")); + + Mpa mpa = new Mpa(); + mpa.setId(rs.getLong("rating_id")); + mpa.setName(rs.getString("mpa_name")); + film.setMpa(mpa); + + film.setLikesByUsers(new HashSet<>()); + film.setGenres(new LinkedHashSet<>()); + + return film; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/FriendshipRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/FriendshipRowMapper.java new file mode 100644 index 0000000..33cb4cc --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/FriendshipRowMapper.java @@ -0,0 +1,20 @@ +package ru.yandex.practicum.filmorate.dal.mappers; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Friendship; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class FriendshipRowMapper implements RowMapper { + + @Override + public Friendship mapRow(ResultSet rs, int rowNum) throws SQLException { + Friendship friendship = new Friendship(); + friendship.setUserId(rs.getLong("user_id")); + friendship.setFriendId(rs.getLong("friend_id")); + return friendship; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/GenreRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/GenreRowMapper.java new file mode 100644 index 0000000..a321799 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/GenreRowMapper.java @@ -0,0 +1,20 @@ +package ru.yandex.practicum.filmorate.dal.mappers; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Genre; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class GenreRowMapper implements RowMapper { + + @Override + public Genre mapRow(ResultSet rs, int rowNum) throws SQLException { + Genre genre = new Genre(); + genre.setId(rs.getLong("genre_id")); + genre.setName(rs.getString("name")); + return genre; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/LikesRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/LikesRowMapper.java new file mode 100644 index 0000000..c5229d1 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/LikesRowMapper.java @@ -0,0 +1,20 @@ +package ru.yandex.practicum.filmorate.dal.mappers; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Likes; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class LikesRowMapper implements RowMapper { + + @Override + public Likes mapRow(ResultSet rs, int rowNum) throws SQLException { + Likes likes = new Likes(); + likes.setFilmId(rs.getLong("film_id")); + likes.setUserId(rs.getLong("user_id")); + return likes; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/MpaRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/MpaRowMapper.java new file mode 100644 index 0000000..9ed8a00 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/MpaRowMapper.java @@ -0,0 +1,20 @@ +package ru.yandex.practicum.filmorate.dal.mappers; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class MpaRowMapper implements RowMapper { + + @Override + public Mpa mapRow(ResultSet rs, int rowNum) throws SQLException { + Mpa mpa = new Mpa(); + mpa.setId(rs.getLong("rating_id")); + mpa.setName(rs.getString("name")); + return mpa; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/UserRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/UserRowMapper.java new file mode 100644 index 0000000..36e492c --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/UserRowMapper.java @@ -0,0 +1,27 @@ +package ru.yandex.practicum.filmorate.dal.mappers; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.User; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashSet; + +@Component +public class UserRowMapper implements RowMapper { + + @Override + public User mapRow(ResultSet rs, int rowNum) throws SQLException { + User user = new User(); + user.setId(rs.getLong("user_id")); + user.setEmail(rs.getString("email")); + user.setLogin(rs.getString("login")); + user.setName(rs.getString("name")); + user.setBirthday(rs.getDate("birthday").toLocalDate()); + + user.setFriends(new HashSet<>()); + + return user; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java new file mode 100644 index 0000000..91a3298 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java @@ -0,0 +1,24 @@ +package ru.yandex.practicum.filmorate.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import ru.yandex.practicum.filmorate.model.*; + +import java.time.LocalDate; +import java.util.Set; + +@Data +public class FilmDto { + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private Long id; + private String name; + private String description; + private LocalDate releaseDate; + private Integer duration; + private Mpa mpa; + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private Set likesByUsers; + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private Set genres; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/NewFilmRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/NewFilmRequest.java new file mode 100644 index 0000000..fa23850 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/NewFilmRequest.java @@ -0,0 +1,21 @@ +package ru.yandex.practicum.filmorate.dto; + +import lombok.Data; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.time.LocalDate; +import java.util.LinkedHashSet; +import java.util.Set; + +@Data +public class NewFilmRequest { + + private Long id; + private String name; + private String description; + private LocalDate releaseDate; + private Integer duration; + private Mpa mpa; + private Set genres = new LinkedHashSet<>(); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/NewUserRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/NewUserRequest.java new file mode 100644 index 0000000..8d3d39e --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/NewUserRequest.java @@ -0,0 +1,18 @@ +package ru.yandex.practicum.filmorate.dto; + +import lombok.Data; + +import java.time.LocalDate; + +@Data +public class NewUserRequest { + + private String email; + private String login; + private String name; + private LocalDate birthday; + + public String getName() { + return (name == null || name.isBlank()) ? login : name; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/UpdateFilmRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/UpdateFilmRequest.java new file mode 100644 index 0000000..2e977a7 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/UpdateFilmRequest.java @@ -0,0 +1,24 @@ +package ru.yandex.practicum.filmorate.dto; + +import lombok.*; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.time.LocalDate; +import java.util.LinkedHashSet; +import java.util.Set; + +@Data +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +public class UpdateFilmRequest { + + private Long id; + private String name; + private String description; + private LocalDate releaseDate; + private Integer duration; + private Mpa mpa; + private Set genres = new LinkedHashSet<>(); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/UpdateUserRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/UpdateUserRequest.java new file mode 100644 index 0000000..682fba3 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/UpdateUserRequest.java @@ -0,0 +1,22 @@ +package ru.yandex.practicum.filmorate.dto; + +import lombok.*; + +import java.time.LocalDate; + +@Data +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +public class UpdateUserRequest { + + private Long id; + private String email; + private String login; + private String name; + private LocalDate birthday; + + public String getName() { + return (name == null || name.isBlank()) ? login : name; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/UserDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/UserDto.java new file mode 100644 index 0000000..c44827f --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/UserDto.java @@ -0,0 +1,24 @@ +package ru.yandex.practicum.filmorate.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.time.LocalDate; +import java.util.Set; + +@Data +public class UserDto { + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private Long id; + private String email; + private String login; + private String name; + private LocalDate birthday; + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private Set friends; + + public String getName() { + return (name == null || name.isBlank()) ? login : name; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/InternalServerException.java b/src/main/java/ru/yandex/practicum/filmorate/exception/InternalServerException.java new file mode 100644 index 0000000..8e1a88b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/InternalServerException.java @@ -0,0 +1,8 @@ +package ru.yandex.practicum.filmorate.exception; + +public class InternalServerException extends RuntimeException { + + public InternalServerException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java new file mode 100644 index 0000000..1b18769 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java @@ -0,0 +1,93 @@ +package ru.yandex.practicum.filmorate.mapper; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import ru.yandex.practicum.filmorate.dto.FilmDto; +import ru.yandex.practicum.filmorate.dto.NewFilmRequest; +import ru.yandex.practicum.filmorate.dto.UpdateFilmRequest; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Genre; + +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.stream.Collectors; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class FilmMapper { + + // Добавление фильма + public static Film mapToFilm(NewFilmRequest request) { + Film film = new Film(); + film.setName(request.getName()); + film.setDescription(request.getDescription()); + film.setReleaseDate(request.getReleaseDate()); + film.setDuration(request.getDuration()); + film.setMpa(request.getMpa()); + film.setGenres(request.getGenres().stream() + .sorted(Comparator.comparing(Genre::getId)) + .collect(Collectors.toCollection(LinkedHashSet::new))); + film.setLikesByUsers(new HashSet<>()); + return film; + } + + // Изменение фильма + public static Film mapToFilm(UpdateFilmRequest request) { + Film film = new Film(); + film.setId(request.getId()); + film.setName(request.getName()); + film.setDescription(request.getDescription()); + film.setReleaseDate(request.getReleaseDate()); + film.setDuration(request.getDuration()); + film.setMpa(request.getMpa()); + film.setGenres(request.getGenres().stream() + .sorted(Comparator.comparing(Genre::getId)) + .collect(Collectors.toCollection(LinkedHashSet::new))); + film.setLikesByUsers(new HashSet<>()); + return film; + } + + // Преобразование в DTO + public static FilmDto mapToDto(Film film) { + FilmDto dto = new FilmDto(); + dto.setId(film.getId()); + dto.setName(film.getName()); + dto.setDescription(film.getDescription()); + dto.setReleaseDate(film.getReleaseDate()); + dto.setDuration(film.getDuration()); + dto.setMpa(film.getMpa()); + + dto.setGenres(film.getGenres() != null ? + new LinkedHashSet<>(film.getGenres()) : + new LinkedHashSet<>()); + + dto.setLikesByUsers(film.getLikesByUsers() != null ? + new HashSet<>(film.getLikesByUsers()) : + new HashSet<>()); + + return dto; + } + + // Частичное изменение фильма + public static Film updateFilmFields(Film film, UpdateFilmRequest request) { + if (request.getName() != null) { + film.setName(request.getName()); + } + if (request.getDescription() != null) { + film.setDescription(request.getDescription()); + } + if (request.getReleaseDate() != null) { + film.setReleaseDate(request.getReleaseDate()); + } + if (request.getDuration() != null) { + film.setDuration(request.getDuration()); + } + if (request.getMpa() != null) { + film.setMpa(request.getMpa()); + } + if (request.getGenres() != null) { + film.setGenres(new LinkedHashSet<>(request.getGenres())); + } + return film; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/UserMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/UserMapper.java new file mode 100644 index 0000000..f66adfe --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/UserMapper.java @@ -0,0 +1,67 @@ +package ru.yandex.practicum.filmorate.mapper; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import ru.yandex.practicum.filmorate.dto.NewUserRequest; +import ru.yandex.practicum.filmorate.dto.UpdateUserRequest; +import ru.yandex.practicum.filmorate.dto.UserDto; +import ru.yandex.practicum.filmorate.model.User; + +import java.util.HashSet; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class UserMapper { + + // Добавление пользователя + public static User mapToUser(NewUserRequest request) { + User user = new User(); + user.setEmail(request.getEmail()); + user.setLogin(request.getLogin()); + user.setName(request.getName()); + user.setBirthday(request.getBirthday()); + user.setFriends(new HashSet<>()); + return user; + } + + // Изменение пользователя + public static User mapToUser(UpdateUserRequest request) { + User user = new User(); + user.setId(request.getId()); + user.setEmail(request.getEmail()); + user.setLogin(request.getLogin()); + user.setBirthday(request.getBirthday()); + user.setFriends(new HashSet<>()); + return user; + } + + // Преобразование в DTO + public static UserDto mapToDto(User user) { + UserDto dto = new UserDto(); + dto.setId(user.getId()); + dto.setEmail(user.getEmail()); + dto.setLogin(user.getLogin()); + dto.setName(user.getName()); + dto.setBirthday(user.getBirthday()); + dto.setFriends(user.getFriends() != null ? + new HashSet<>(user.getFriends()) : + new HashSet<>()); + return dto; + } + + // Частичное изменение пользователя + public static User updateUserFields(User user, UpdateUserRequest request) { + if (request.getEmail() != null) { + user.setEmail(request.getEmail()); + } + if (request.getLogin() != null) { + user.setLogin(request.getLogin()); + } + if (request.getName() != null) { + user.setName(request.getName()); + } + if (request.getBirthday() != null) { + user.setBirthday(request.getBirthday()); + } + return user; + } +} \ 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 b2514ef..16c9d84 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -14,13 +14,11 @@ public class Film { private Long id; - private Set likesByUsers; - private String name; - private String description; - private LocalDate releaseDate; - private Integer duration; + private Mpa mpa; + private Set likesByUsers; + private Set genres; } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Friendship.java b/src/main/java/ru/yandex/practicum/filmorate/model/Friendship.java new file mode 100644 index 0000000..8376838 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Friendship.java @@ -0,0 +1,10 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.Data; + +@Data +public class Friendship { + + private Long userId; + private Long friendId; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java b/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java new file mode 100644 index 0000000..f9aa66e --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java @@ -0,0 +1,12 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(of = {"id"}) +public class Genre { + + private Long id; + private String name; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Likes.java b/src/main/java/ru/yandex/practicum/filmorate/model/Likes.java new file mode 100644 index 0000000..6b89aca --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Likes.java @@ -0,0 +1,9 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.Data; + +@Data +public class Likes { + private Long filmId; + private Long userId; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Mpa.java b/src/main/java/ru/yandex/practicum/filmorate/model/Mpa.java new file mode 100644 index 0000000..d106e01 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Mpa.java @@ -0,0 +1,12 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(of = {"id"}) +public class Mpa { + + private Long id; + private String name; +} \ 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 index b51ef9e..1625a49 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/User.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java @@ -14,15 +14,11 @@ public class User { private Long id; - private Set friends; - private String email; - private String login; - private String name; - private LocalDate birthday; + private Set friends; public String getName() { return (name == null || name.isBlank()) ? login : name; 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 f01a054..c4eca69 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -3,10 +3,13 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dto.FilmDto; +import ru.yandex.practicum.filmorate.dto.NewFilmRequest; +import ru.yandex.practicum.filmorate.dto.UpdateFilmRequest; import ru.yandex.practicum.filmorate.exception.*; +import ru.yandex.practicum.filmorate.mapper.FilmMapper; import ru.yandex.practicum.filmorate.model.*; -import ru.yandex.practicum.filmorate.storage.film.FilmStorage; -import ru.yandex.practicum.filmorate.storage.user.UserStorage; +import ru.yandex.practicum.filmorate.storage.*; import ru.yandex.practicum.filmorate.utils.FilmValidate; import java.util.*; @@ -19,68 +22,130 @@ public class FilmService { private final FilmStorage filmStorage; private final UserStorage userStorage; private final FilmValidate filmValidate; + private final GenreStorage genreStorage; + private final MpaStorage mpaStorage; + private final LikesStorage likesStorage; + + // Получение всех фильмов + public Collection findAll() { + return filmStorage.findAll().stream() + .map(FilmMapper::mapToDto) + .toList(); + } - public Collection findAll() { - return filmStorage.findAll(); + // Получение фильма по id + public FilmDto findById(Long filmId) { + return filmStorage.findById(filmId) + .map(FilmMapper::mapToDto) + .orElseThrow(() -> new NotFoundException("Фильм не найден")); } - public Film create(Film film) { + // Добавление фильма + public FilmDto create(NewFilmRequest request) { + Film film = FilmMapper.mapToFilm(request); filmValidate.validateFilm(film); - return filmStorage.create(film); + + if (film.getGenres() != null) { + film.getGenres().forEach(genre -> { + genreStorage.findById(genre.getId()) + .orElseThrow(() -> { + log.warn("Жанр с id:{} не найден", genre.getId()); + return new NotFoundException("Жанр не найден"); + }); + }); + } + + Long mpaId = film.getMpa().getId(); + if (mpaId != null) { + mpaStorage.findMpaById(mpaId) + .orElseThrow(() -> { + log.warn("Рейтинг с id:{} не найден", mpaId); + return new NotFoundException("Рейтинг MPA не найден"); + }); + } + + film = filmStorage.create(film); + return FilmMapper.mapToDto(film); } - public Film update(Film film) { + // Обновление фильма + public FilmDto update(UpdateFilmRequest request) { + Film film = FilmMapper.mapToFilm(request); filmValidate.validateFilm(film); - return filmStorage.update(film); - } - public void delete(Long id) { - filmStorage.delete(id); + Film oldFilm = filmStorage.findById(request.getId()) + .orElseThrow(() -> { + log.warn("Фильм с id:{} не найден", request.getId()); + return new NotFoundException("Фильм не найден"); + }); + + Long mpaId = film.getMpa().getId(); + if (mpaId != null) { + mpaStorage.findMpaById(mpaId) + .orElseThrow(() -> { + log.warn("Рейтинг с id:{} не найден", mpaId); + return new NotFoundException("Рейтинг MPA не найден"); + }); + } + + if (film.getGenres() != null) { + film.getGenres().forEach(genre -> { + genreStorage.findById(genre.getId()) + .orElseThrow(() -> { + log.warn("Жанр с id:{} не найден", genre.getId()); + return new NotFoundException("Жанр не найден"); + }); + }); + } + + film = FilmMapper.updateFilmFields(oldFilm, request); + film.setLikesByUsers(oldFilm.getLikesByUsers()); + return FilmMapper.mapToDto(filmStorage.update(film)); } - public Film getById(Long id) { - return filmStorage.getById(id); + // Удаление фильма по id + public void delete(Long filmId) { + filmStorage.delete(filmId); } // Метод добавления лайка фильму(по id) от пользователя c указанным id public void addLike(Long filmId, Long userId) { - final Film film = filmStorage.getById(filmId); - if (film == null) { - log.warn("Попытка лайкнуть несуществующий фильм Id:{}", filmId); - throw new NotFoundException("Фильм не найден"); - } - - final User user = userStorage.getById(userId); - if (user == null) { - log.warn("Попытка лайка от несуществующего пользователя Id:{}", userId); - throw new NotFoundException("Пользователь не найден"); - } + final Film film = filmStorage.findById(filmId) + .orElseThrow(() -> { + log.warn("Попытка лайкнуть несуществующий фильм Id:{}", filmId); + throw new NotFoundException("Фильм не найден"); + }); + + final User user = userStorage.findById(userId) + .orElseThrow(() -> { + log.warn("Попытка лайка от несуществующего пользователя Id:{}", userId); + throw new NotFoundException("Пользователь не найден"); + }); Set likes = film.getLikesByUsers(); if (likes.contains(userId)) { log.debug("Пользователь с Id:{} уже ставил лайк фильму с Id:{}", userId, filmId); return; } - likes.add(userId); film.setLikesByUsers(likes); - filmStorage.update(film); + likesStorage.addLikeToFilm(filmId, userId); log.debug("Пользователь с Id:{} поставил лайк фильму c Id:{}", userId, filmId); } // Метод удаления лайка у фильма(по id) от пользователя с указанным id public void removeLike(Long filmId, Long userId) { - final Film film = filmStorage.getById(filmId); - if (film == null) { - log.warn("Попытка удалить лайк у несуществующего фильма с Id:{}", filmId); - throw new NotFoundException("Фильм не найден"); - } - - final User user = userStorage.getById(userId); - if (user == null) { - log.warn("Попытка удалить лайк от несуществующего пользователя c Id:{}", userId); - throw new NotFoundException("Пользователь не найден"); - } + final Film film = filmStorage.findById(filmId) + .orElseThrow(() -> { + log.warn("Попытка удалить лайк у несуществующего фильма с Id:{}", filmId); + throw new NotFoundException("Фильм не найден"); + }); + + final User user = userStorage.findById(userId) + .orElseThrow(() -> { + log.warn("Попытка удалить лайк от несуществующего пользователя c Id:{}", userId); + throw new NotFoundException("Пользователь не найден"); + }); Set likes = film.getLikesByUsers(); if (!likes.contains(userId)) { @@ -89,20 +154,17 @@ public void removeLike(Long filmId, Long userId) { } likes.remove(userId); film.setLikesByUsers(likes); - filmStorage.update(film); + likesStorage.removeLikeFromFilm(filmId, userId); log.debug("Пользователь с Id:{} удалил лайк фильму c Id:{}", userId, filmId); } - // Метод вывода 10 популярных фильмов по количеству лайков - public Collection getPopularFilms(int count) { - if (count <= 0) { - throw new ValidationException("Количество фильмов должно быть положительным"); - } - + // Метод получения популярных фильмов по количеству лайков + public Collection findPopularFilms(int count) { Collection films = filmStorage.findAll(); return films.stream() .sorted((f1, f2) -> f2.getLikesByUsers().size() - f1.getLikesByUsers().size()) .limit(count) + .map(FilmMapper::mapToDto) .toList(); } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java b/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java new file mode 100644 index 0000000..ceb291b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java @@ -0,0 +1,30 @@ +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.model.Genre; +import ru.yandex.practicum.filmorate.storage.GenreStorage; + +import java.util.Collection; + +@Slf4j +@Service +public class GenreService { + + @Autowired + private GenreStorage genreStorage; + + public Collection findAll() { + return genreStorage.findAllGenres(); + } + + public Genre findById(Long genreId) { + return genreStorage.findById(genreId) + .orElseThrow(() -> { + log.warn("Жанр с id:{} не найден", genreId); + return new NotFoundException("Жанр не найден"); + }); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java b/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java new file mode 100644 index 0000000..a599f88 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java @@ -0,0 +1,30 @@ +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.model.Mpa; +import ru.yandex.practicum.filmorate.storage.MpaStorage; + +import java.util.Collection; + +@Slf4j +@Service +public class MpaService { + + @Autowired + private MpaStorage mpaStorage; + + public Collection findAll() { + return mpaStorage.findAll(); + } + + public Mpa findById(Long ratingId) { + return mpaStorage.findMpaById(ratingId) + .orElseThrow(() -> { + log.warn("Рейтинг с id:{} не найден", ratingId); + return new NotFoundException("Рейтинг MPA не найден"); + }); + } +} \ 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 index c2e60f6..7346bed 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -3,13 +3,17 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dto.NewUserRequest; +import ru.yandex.practicum.filmorate.dto.UpdateUserRequest; +import ru.yandex.practicum.filmorate.dto.UserDto; import ru.yandex.practicum.filmorate.exception.*; +import ru.yandex.practicum.filmorate.mapper.UserMapper; import ru.yandex.practicum.filmorate.model.User; -import ru.yandex.practicum.filmorate.storage.user.UserStorage; +import ru.yandex.practicum.filmorate.storage.FriendshipStorage; +import ru.yandex.practicum.filmorate.storage.UserStorage; import ru.yandex.practicum.filmorate.utils.UserValidate; import java.util.*; -import java.util.stream.Collectors; @Slf4j @Service @@ -18,134 +22,144 @@ public class UserService { private final UserStorage userStorage; private final UserValidate userValidate; + private final FriendshipStorage friendshipStorage; - public Collection findAll() { - return userStorage.findAll(); + // Получение всех пользователей + public Collection findAll() { + return userStorage.findAll().stream() + .map(UserMapper::mapToDto) + .toList(); } - public User create(User user) { - userValidate.validateUser(user); - return userStorage.create(user); + // Получение пользователя по id + public UserDto getById(Long userId) { + return userStorage.findById(userId) + .map(UserMapper::mapToDto) + .orElseThrow(() -> new NotFoundException("Пользователь не найден")); } - public User update(User user) { + // Добавление пользователя + public UserDto create(NewUserRequest request) { + User user = UserMapper.mapToUser(request); userValidate.validateUser(user); - return userStorage.update(user); + + User createdUser = userStorage.create(user); + return UserMapper.mapToDto(createdUser); } - public void delete(Long id) { - userStorage.delete(id); + // Обновление пользователя + public UserDto update(UpdateUserRequest request) { + User user = UserMapper.mapToUser(request); + userValidate.validateUser(user); + + User oldUser = userStorage.findById(request.getId()) + .orElseThrow(() -> { + log.warn("Пользователь с id:{} не найден", request.getId()); + return new NotFoundException("Пользователь не найден"); + }); + user = UserMapper.updateUserFields(oldUser, request); + return UserMapper.mapToDto(userStorage.update(user)); } - public User getById(Long id) { - return userStorage.getById(id); + // Удаление пользователя + public void delete(Long userId) { + userStorage.delete(userId); } // Метод добавления пользователей в друзья public void addFriend(Long userId, Long friendId) { - final User user = userStorage.getById(userId); - if (user == null) { - log.warn("Попытка несуществующего пользователя с Id:{} добавить в друзья пользователей", userId); - throw new NotFoundException("Пользователь не найден"); - } - - final User friend = userStorage.getById(friendId); - if (friend == null) { - log.warn("Попытка добавить в друзья несуществующего пользователя с Id:{}", friendId); - throw new NotFoundException("Пользователь не найден"); - } + final User user = userStorage.findById(userId) + .orElseThrow(() -> { + log.warn("Попытка несуществующего пользователя с Id:{} добавить в кого-то в друзья", userId); + throw new NotFoundException("Пользователь не найден"); + }); + + final User friend = userStorage.findById(friendId) + .orElseThrow(() -> { + log.warn("Попытка добавить в друзья несуществующего пользователя с Id:{}", friendId); + throw new NotFoundException("Пользователь не найден"); + }); if (user.equals(friend)) { log.warn("Попытка добавить самого себя в друзья"); throw new DuplicateException("Нельзя добавить самого себя в друзья"); } - Set userFriends = user.getFriends(); - if (userFriends.contains(friendId)) { - log.debug("Пользователь с Id:{} уже добавил в друзья пользователя с Id:{}", userId, friendId); - return; + if (user.getFriends().contains(friendId)) { + log.warn("Попытка пользователя с Id:{} снова добавить в друзья пользователя с Id:{}", userId, friendId); + throw new DuplicateException("Пользователь уже отправлял запрос на дружбу"); } - - userFriends.add(friendId); - user.setFriends(userFriends); + user.getFriends().add(friendId); userStorage.update(user); - Set friendFriends = friend.getFriends(); - friendFriends.add(userId); - friend.setFriends(friendFriends); - userStorage.update(friend); + friendshipStorage.addFriend(userId, friendId); log.debug("Пользователи с Id:{} и Id:{} теперь друзья", userId, friendId); } // Метод удаления пользователей из друзей public void removeFriend(Long userId, Long friendId) { - final User user = userStorage.getById(userId); - if (user == null) { - log.warn("Попытка несуществующего пользователя с Id:{} удалить кого-то из друзей", userId); - throw new NotFoundException("Пользователь не найден"); - } - - final User friend = userStorage.getById(friendId); - if (friend == null) { - log.warn("Попытка удалить из друзей несуществующего пользователя с Id:{}", friendId); - throw new NotFoundException("Пользователь не найден"); - } + final User user = userStorage.findById(userId) + .orElseThrow(() -> { + log.warn("Попытка несуществующего пользователя с Id:{} удалить кого-то из друзей", userId); + throw new NotFoundException("Пользователь не найден"); + }); + + final User friend = userStorage.findById(friendId) + .orElseThrow(() -> { + log.warn("Попытка удалить из друзей несуществующего пользователя с Id:{}", friendId); + throw new NotFoundException("Пользователь не найден"); + }); if (user.equals(friend)) { log.warn("При удалении из друзей были переданы одинаковые Id: userId={}, friendId={}", userId, friendId); throw new DuplicateException("Нельзя удалить самого себя из друзей"); } - Set userFriends = user.getFriends(); - if (!userFriends.contains(friendId)) { - log.debug("Пользователь с Id:{} не добавлял в друзья пользователя с Id:{}", userId, friendId); + if (!user.getFriends().contains(friendId)) { + log.warn("Пользователь с Id:{} не добавлял в друзья пользователя с Id:{}", userId, friendId); return; } - userFriends.remove(friendId); - user.setFriends(userFriends); + user.getFriends().remove(friendId); userStorage.update(user); - Set friendFriends = friend.getFriends(); - friendFriends.remove(userId); - friend.setFriends(friendFriends); - userStorage.update(friend); + friendshipStorage.removeFriend(userId, friendId); log.debug("Пользователи с Id:{} и Id:{} больше не друзья", userId, friendId); } // Метод получения списка всех друзей пользователя - public Collection getFriends(Long id) { - final User user = userStorage.getById(id); - - if (user == null) { - log.warn("Попытка получить друзей несуществующего пользователя c Id:{}", id); - throw new NotFoundException("Пользователь не найден"); - } + public Collection findAllFriends(Long userId) { + final User user = userStorage.findById(userId) + .orElseThrow(() -> { + log.warn("Попытка получить друзей несуществующего пользователя c Id:{}", userId); + throw new NotFoundException("Пользователь не найден"); + }); // Возвращаем пустую коллекцию, если у пользователя нет друзей - if (user.getFriends().isEmpty()) { - log.debug("У пользователя с Id:{} нет друзей", id); + if (user.getFriends() == null || user.getFriends().isEmpty()) { + log.debug("У пользователя с Id:{} нет друзей", userId); return Collections.emptyList(); } - return user.getFriends().stream() - .map(userStorage::getById) - .collect(Collectors.toList()); + return userStorage.findAll().stream() + .filter(u -> user.getFriends().contains(u.getId())) + .map(UserMapper::mapToDto) + .toList(); } // Метод получения общих друзей - public Collection getCommonFriends(Long userId, Long friendId) { - final User user = getById(userId); - if (user == null) { - log.warn("Попытка получить общих друзей у несуществующего пользователя c Id:{}", userId); - throw new NotFoundException("Пользователь не найден"); - } - - final User friend = getById(friendId); - if (friend == null) { - log.warn("Попытка получить общих друзей с несуществующим пользователем с Id:{}",friendId); - throw new NotFoundException("Пользователь не найден"); - } + public Collection findCommonFriends(Long userId, Long friendId) { + final User user = userStorage.findById(userId) + .orElseThrow(() -> { + log.warn("Попытка получить общих друзей у несуществующего пользователя c Id:{}", userId); + throw new NotFoundException("Пользователь не найден"); + }); + + final User friend = userStorage.findById(friendId) + .orElseThrow(() -> { + log.warn("Попытка получить общих друзей с несуществующим пользователем с Id:{}",friendId); + throw new NotFoundException("Пользователь не найден"); + }); // Поиск пересечения друзей Set intersection = new HashSet<>(user.getFriends()); @@ -157,6 +171,7 @@ public Collection getCommonFriends(Long userId, Long friendId) { return userStorage.findAll().stream() .filter(u -> intersection.contains(u.getId())) + .map(UserMapper::mapToDto) .toList(); } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java similarity index 51% rename from src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java rename to src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java index 51f315e..e5350ef 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java @@ -1,18 +1,21 @@ -package ru.yandex.practicum.filmorate.storage.film; +package ru.yandex.practicum.filmorate.storage; import ru.yandex.practicum.filmorate.model.Film; import java.util.Collection; +import java.util.Optional; public interface FilmStorage { Film create(Film film); - void delete(Long id); + void delete(Long filmId); Film update(Film film); - Film getById(Long id); + Optional findById(Long filmId); Collection findAll(); + + Collection findPopularFilms(int count); } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/FriendshipStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/FriendshipStorage.java new file mode 100644 index 0000000..7c30f84 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/FriendshipStorage.java @@ -0,0 +1,16 @@ +package ru.yandex.practicum.filmorate.storage; + +import ru.yandex.practicum.filmorate.model.User; + +import java.util.Collection; + +public interface FriendshipStorage { + + void addFriend(Long userId, Long friendId); + + void removeFriend(Long userId, Long friendId); + + Collection findAllFriends(Long userId); + + Collection findCommonFriends(Long userId, Long friendId); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/GenreStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/GenreStorage.java new file mode 100644 index 0000000..ef6faa9 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/GenreStorage.java @@ -0,0 +1,17 @@ +package ru.yandex.practicum.filmorate.storage; + +import ru.yandex.practicum.filmorate.model.Genre; + +import java.util.Collection; +import java.util.Optional; + +public interface GenreStorage { + + Collection findAllGenres(); + + Optional findById(Long genreId); + + Collection findGenreForFilm(Long filmId); + + void setGenreForFilm(Long filmId, Collection genres); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/LikesStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/LikesStorage.java new file mode 100644 index 0000000..23cfd82 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/LikesStorage.java @@ -0,0 +1,14 @@ +package ru.yandex.practicum.filmorate.storage; + +import ru.yandex.practicum.filmorate.model.Likes; + +import java.util.Collection; + +public interface LikesStorage { + + void addLikeToFilm(Long filmId, Long userId); + + void removeLikeFromFilm(Long filmId, Long userId); + + Collection getLikesOnFilm(Long filmId); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/MpaStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/MpaStorage.java new file mode 100644 index 0000000..21efe32 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/MpaStorage.java @@ -0,0 +1,15 @@ +package ru.yandex.practicum.filmorate.storage; + +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.util.Collection; +import java.util.Optional; + +public interface MpaStorage { + + Collection findAll(); + + Optional findMpaById(Long ratingId); + + Optional findMpaFilm(Long filmId); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java similarity index 58% rename from src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java rename to src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java index 95a43e2..74b5430 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java @@ -1,18 +1,19 @@ -package ru.yandex.practicum.filmorate.storage.user; +package ru.yandex.practicum.filmorate.storage; import ru.yandex.practicum.filmorate.model.User; import java.util.Collection; +import java.util.Optional; public interface UserStorage { User create(User user); - void delete(Long id); + void delete(Long findId); User update(User user); - User getById(Long id); + Optional findById(Long userId); Collection findAll(); } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/utils/FilmValidate.java b/src/main/java/ru/yandex/practicum/filmorate/utils/FilmValidate.java index 1155956..ddee851 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/utils/FilmValidate.java +++ b/src/main/java/ru/yandex/practicum/filmorate/utils/FilmValidate.java @@ -39,5 +39,10 @@ public static void validateFilm(Film film) { log.error("Попытка добавить фильм с некорректной продолжительностью"); throw new ValidationException("Недопустимая продолжительность фильма"); } + + if (film.getMpa() == null || film.getMpa().getId() == null) { + log.error("Попытка добавить фильм без рейтинга"); + throw new ValidationException("Mpa-рейтинг фильма не может быть пустым"); + } } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/utils/UserValidate.java b/src/main/java/ru/yandex/practicum/filmorate/utils/UserValidate.java index a76a93c..5f2ce5b 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/utils/UserValidate.java +++ b/src/main/java/ru/yandex/practicum/filmorate/utils/UserValidate.java @@ -28,6 +28,11 @@ public static void validateUser(User user) { throw new ValidationException("Дата рождения не может быть в будущем"); } + if (user.getBirthday() == null) { + log.error("Попытка ввести пустую дату рождения"); + throw new ValidationException("Дата рождения не может быть null"); + } + if (user.getEmail().isEmpty()) { log.error("Попытка добавить пустой email"); throw new ValidationException("email не может быть пустым"); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4a23bbc..a91ce0b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,2 +1,7 @@ server.port=8080 -logging.level.org.zalando.logbook=TRACE \ No newline at end of file +logging.level.org.zalando.logbook=TRACE +spring.sql.init.mode=always +spring.datasource.url=jdbc:h2:file:./db/filmorate;MODE=PostgreSQL +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 0000000..b121b6a --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,16 @@ +MERGE INTO genre (genre_id, name) +VALUES + (1, 'Комедия'), + (2, 'Драма'), + (3, 'Мультфильм'), + (4, 'Триллер'), + (5, 'Документальный'), + (6, 'Боевик'); + +MERGE INTO mpa_rating (rating_id, name) +VALUES + (1, 'G'), + (2, 'PG'), + (3, 'PG-13'), + (4, 'R'), + (5, 'NC-17'); \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..afe2d46 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,51 @@ +CREATE TABLE IF NOT EXISTS mpa_rating ( + rating_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name varchar(5) NOT NULL +); + +CREATE TABLE IF NOT EXISTS films ( + film_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name varchar(200) NOT NULL, + description varchar(200), + release_date date NOT NULL, + duration bigint NOT NULL, + rating_id bigint NOT NULL, + FOREIGN KEY (rating_id) REFERENCES mpa_rating (rating_id) +); + +CREATE TABLE IF NOT EXISTS users ( + user_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + email varchar NOT NULL, + login varchar NOT NULL, + name varchar, + birthday date NOT NULL +); + +CREATE TABLE IF NOT EXISTS likes ( + film_id bigint, + user_id bigint, + PRIMARY KEY (film_id, user_id), + FOREIGN KEY (film_id) REFERENCES films (film_id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS friendship ( + user_id bigint, + friend_id bigint, + PRIMARY KEY (user_id, friend_id), + FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE, + FOREIGN KEY (friend_id) REFERENCES users (user_id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS genre ( + genre_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name varchar(50) NOT NULL +); + +CREATE TABLE IF NOT EXISTS film_genres ( + film_id bigint, + genre_id bigint, + PRIMARY KEY (film_id, genre_id), + FOREIGN KEY (film_id) REFERENCES films (film_id) ON DELETE CASCADE, + FOREIGN KEY (genre_id) REFERENCES genre (genre_id) +); \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java b/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java index be537be..e188d8d 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java +++ b/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java @@ -3,10 +3,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -@SpringBootTest +@SpringBootTest(classes = FilmorateApplication.class) class FilmorateApplicationTests { @Test void contextLoads() { } -} +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/dal/FilmDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/dal/FilmDbStorageTest.java new file mode 100644 index 0000000..c3dac0c --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/dal/FilmDbStorageTest.java @@ -0,0 +1,224 @@ +package ru.yandex.practicum.filmorate.dal; + +import lombok.RequiredArgsConstructor; +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.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.context.annotation.Import; +import org.springframework.jdbc.core.JdbcTemplate; +import ru.yandex.practicum.filmorate.dal.mappers.*; +import ru.yandex.practicum.filmorate.model.*; + +import java.time.LocalDate; +import java.util.*; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@Import({ + FilmDbStorage.class, + GenreDbStorage.class, + LikesDbStorage.class, + MpaDbStorage.class, + FilmRowMapper.class, + GenreRowMapper.class, + LikesRowMapper.class, + MpaRowMapper.class}) +class FilmDbStorageTest { + + private final FilmDbStorage filmDbStorage; + private final JdbcTemplate jdbcTemplate; + private final LikesDbStorage likesDbStorage; + + @BeforeEach + public void beforeEach() { + // Очистка данных + jdbcTemplate.execute("DELETE FROM film_genres"); + jdbcTemplate.execute("DELETE FROM likes"); + jdbcTemplate.execute("DELETE FROM films"); + jdbcTemplate.execute("DELETE FROM mpa_rating"); + jdbcTemplate.execute("DELETE FROM genre"); + + // Инициализация mpa и жанров для тестов + jdbcTemplate.update("INSERT INTO mpa_rating (rating_id, name) VALUES (1, 'G'), (2, 'PG')"); + jdbcTemplate.update("INSERT INTO genre (genre_id, name) VALUES (1, 'Комедия'), (2, 'Драма')"); + + // Создание тестовых пользователей + jdbcTemplate.update("INSERT INTO users (user_id, email, login, name, birthday) VALUES " + + "(1, 'user@mail.ru', 'login1', 'user_login', '1995-02-13'), " + + "(2, 'user@yandex.ru', 'login2', 'user2_login', '1996-02-14')"); + } + + // Тест создания фильма с валидными данными и получения его по id + @Test + public void createFilm_withValidFields_return_filmAddedWithGeneratedId() { + Film film = createTestFilm("Фильм", "Описание фильма", + LocalDate.of(1995, 2, 13), 120, 1L, Set.of(1L)); + + // Сохранение фильма в БД и поиск по id + Film newFilm = filmDbStorage.create(film); + Optional filmOptional = filmDbStorage.findById(newFilm.getId()); + assertTrue(filmOptional.isPresent(), "Фильм должен существовать"); + + // Получение фильма из Optional + Film retrievedFilm = filmOptional.get(); + + // Проверка основных полей фильма + assertFilmEquals(film, filmOptional.get()); + + // Проверка связей фильма и MPA + assertNotNull(retrievedFilm.getMpa(), "MPA не должен быть null"); + assertEquals(1L, retrievedFilm.getMpa().getId(), "id MPA должен совпадать"); + assertEquals("G", retrievedFilm.getMpa().getName(), "Название MPA должно быть 'G'"); + + // Проверка связей фильма и жанров + assertNotNull(retrievedFilm.getGenres(), "Жанры не должны быть null"); + assertEquals(1, retrievedFilm.getGenres().size(), "Должен быть 1 жанр"); + assertEquals(1L, retrievedFilm.getGenres().iterator().next().getId(), + "id жанра должно быть 1"); + } + + // Тест получения списка всех созданных фильмов + @Test + public void findAll_afterAddingTwoFilms_returnsCollectionSize2() { + Film film = createTestFilm("Фильм", "Описание фильма", + LocalDate.of(1995, 2, 13), 120, 1L, Set.of(1L)); + Film film2 = createTestFilm("Фильм 2", "Описание фильма 2", + LocalDate.of(2001, 3, 21), 145, 2L, Set.of(1L, 2L)); + + // Сохранение фильмов в БД + filmDbStorage.create(film); + filmDbStorage.create(film2); + + Collection films = filmDbStorage.findAll(); + assertEquals(2, films.size(), "Должны быть найдены 2 фильма"); + } + + // Тест обновления фильма + @Test + public void update_existingFilmWithValidData_fieldsUpdated() { + Film film = createTestFilm("Фильм", "Описание фильма", + LocalDate.of(1995, 2, 13), 120, 1L, Collections.emptySet()); + + // Сохранение фильма в БД + Film createdFilm = filmDbStorage.create(film); + + // Обновление данных фильма + Film updatedFilm = createTestFilm("Фильм 2", "Описание фильма 2", + LocalDate.of(2001, 3, 21), 145, 2L, Set.of(1L, 2L)); + updatedFilm.setId(createdFilm.getId()); + + //Обновление фильма в БД и получение его из Optional + filmDbStorage.update(updatedFilm); + Film retrievedFilm = filmDbStorage.findById(createdFilm.getId()).orElseThrow(); + + // Проверка основных полей фильма + assertFilmEquals(updatedFilm, retrievedFilm); + + // Проверка связей фильма и MPA + assertNotNull(retrievedFilm.getMpa(), "MPA не должен быть null"); + assertEquals(2L, retrievedFilm.getMpa().getId(), "id MPA должен совпадать"); + assertEquals("PG", retrievedFilm.getMpa().getName(), "Название MPA должно быть 'PG'"); + + // Проверка связей фильма и жанров + assertNotNull(retrievedFilm.getGenres(), "Жанры не должны быть null"); + assertEquals(2, retrievedFilm.getGenres().size(), "Должно быть 2 жанра"); + } + + // Тест удаления фильма по id + @Test + public void delete_existingFilm_removesFilm() { + Film film = createTestFilm("Фильм", "Описание фильма", + LocalDate.of(1995, 2, 13), 120, 1L, Collections.emptySet()); + + // Сохранение фильма в БД + filmDbStorage.create(film); + + // Удаление фильма + filmDbStorage.delete(film.getId()); + Optional filmOptional = filmDbStorage.findById(film.getId()); + assertEquals(Optional.empty(), filmOptional, "Созданный фильм должен быть удален"); + } + + // Тест на получение списка популярных фильмов + @Test + public void getPopularFilms_returnsFilmsOrderedByLikes() { + Film film1 = createTestFilm("Фильм 1", "Описание 1", + LocalDate.of(2000, 1, 1), 100, 1L, Set.of(1L)); + Film film2 = createTestFilm("Фильм 2", "Описание 2", + LocalDate.of(2001, 1, 1), 120, 2L, Set.of(2L)); + + // Сохранение фильмов в БД + Film createdFilm = filmDbStorage.create(film1); + Film createdFilm2 = filmDbStorage.create(film2); + + // Добавление лайков (film2 - 2 лайка, film1 - 0 лайков) + likesDbStorage.addLikeToFilm(createdFilm2.getId(), 1L); + likesDbStorage.addLikeToFilm(createdFilm2.getId(), 2L); + likesDbStorage.addLikeToFilm(createdFilm.getId(), 2L); + + Collection popularFilms = filmDbStorage.findPopularFilms(2); + + assertNotNull(popularFilms, "Список не должен быть null"); + assertEquals(2, popularFilms.size(), "Должны вернуться 2 фильма"); + + // Преобразование в список для проверки порядка + List filmsList = new ArrayList<>(popularFilms); + + // Проверка порядка сортировки (по убыванию лайков) + assertEquals(createdFilm2.getId(), filmsList.get(0).getId(), + "Первым должен быть фильм с наибольшим количеством лайков"); + assertEquals(2, filmsList.get(0).getLikesByUsers().size(), + "У первого фильма должно быть 2 лайка"); + + assertEquals(createdFilm.getId(), filmsList.get(1).getId(), + "Вторым должен быть фильм с меньшим количеством лайков"); + assertEquals(1, filmsList.get(1).getLikesByUsers().size(), + "У второго фильма должен быть 1 лайк"); + } + + // Вспомогательный метод для создания тестового фильма + private Film createTestFilm( + String name, + String description, + LocalDate release_date, + int duration, + Long mpaId, + Set genresId) { + Film film = new Film(); + film.setName(name); + film.setDescription(description); + film.setReleaseDate(release_date); + film.setDuration(duration); + + Mpa mpa = new Mpa(); + mpa.setId(mpaId); + film.setMpa(mpa); + + + Set genres = genresId.stream() + .map(id -> { + Genre g = new Genre(); + g.setId(id); + return g; + }) + .collect(Collectors.toSet()); + film.setGenres(genres); + + return film; + } + + // Вспомогательный метод для проверки основных полей фильма + private void assertFilmEquals(Film expected, Film actual) { + assertEquals(expected.getName(), actual.getName(), "Название фильма должно совпадать"); + assertEquals(expected.getDescription(), actual.getDescription(), "Описание должно совпадать"); + assertEquals(expected.getReleaseDate(), actual.getReleaseDate(), "Дата релиза должна совпадать"); + assertEquals(expected.getDuration(), actual.getDuration(), + "Продолжительность фильма должна совпадать"); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/dal/FriendshipDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/dal/FriendshipDbStorageTest.java new file mode 100644 index 0000000..7c19280 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/dal/FriendshipDbStorageTest.java @@ -0,0 +1,86 @@ +package ru.yandex.practicum.filmorate.dal; + +import lombok.RequiredArgsConstructor; +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.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.context.annotation.Import; +import org.springframework.jdbc.core.JdbcTemplate; +import ru.yandex.practicum.filmorate.dal.mappers.FriendshipRowMapper; +import ru.yandex.practicum.filmorate.dal.mappers.UserRowMapper; +import ru.yandex.practicum.filmorate.model.User; + +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; + +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@Import({FriendshipDbStorage.class, FriendshipRowMapper.class, UserDbStorage.class, UserRowMapper.class}) +public class FriendshipDbStorageTest { + + private final FriendshipDbStorage friendshipDbStorage; + private final JdbcTemplate jdbcTemplate; + + @BeforeEach + public void beforeEach() { + jdbcTemplate.execute("DELETE FROM friendship"); + jdbcTemplate.execute("DELETE FROM users"); + + // Создание тестовых пользователей + jdbcTemplate.update("INSERT INTO users (user_id, email, login, name, birthday) VALUES " + + "(1, 'user@yandex.ru', 'Mult1k', 'Дмитрий', '1995-02-13'), " + + "(2, 'user@gmail.com.ru', 'Friend', 'Саша', '1995-04-24'), " + + "(3, 'user@yahoo.com', 'Friend2', 'Ксения', '1996-02-14')"); + } + + // Тест добавления в друзья и получение списка всех друзей + @Test + public void addFriend_addFriendship() { + // Добавление пользователем user в друзья пользователей friend и friend2 + friendshipDbStorage.addFriend(1L, 2L); + friendshipDbStorage.addFriend(1L, 3L); + + // Получение списка друзей пользователя user + Collection friends = friendshipDbStorage.findAllFriends(1L); + assertEquals(2, friends.size(), "У пользователя должно быть 2 друга"); + + // Получение списка друзей пользователей friend и friend2 - список должен быть пуст, тк дружба односторонняя + Collection friendFriends = friendshipDbStorage.findAllFriends(2L); + assertEquals(0, friendFriends.size(), "У пользователя friend не должно быть друзей"); + Collection friendFriends2 = friendshipDbStorage.findAllFriends(3L); + assertEquals(0, friendFriends2.size(), "У пользователя friend2 не должно быть друзей"); + } + + // Тест удаления пользователя из друзей + @Test + public void delete_existingFriend_removesFriend() { + // Добавление пользователем user в друзья пользователя friend + friendshipDbStorage.addFriend(1L, 2L); + + // Удаление пользователем user из друзей пользователя friend + friendshipDbStorage.removeFriend(1L, 2L); + + // Получение списка друзей пользователя user + Collection friends = friendshipDbStorage.findAllFriends(1L); + assertTrue(friends.isEmpty(), "У пользователя не должно быть друзей"); + } + + // Тест получения общего списка друзей + @Test + public void getCommonFriends_returnsCommonFriends() { + // Добавление пользователями с id 1L и 2L в друзья пользователя с id 3L + friendshipDbStorage.addFriend(1L, 3L); + friendshipDbStorage.addFriend(2L, 3L); + + // Получение списка общего друга(Friend2) у пользователей User и Friend + Collection commonFriends = friendshipDbStorage.findCommonFriends(1L, 2L); + assertEquals(1, commonFriends.size(), + "У пользователей Mult1k и Friend должен быть 1 общий друг"); + assertFalse(commonFriends.isEmpty(), "Список общих друзей пуст"); + assertEquals(3L, commonFriends.iterator().next().getId(), "Неверный id общего друга"); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/dal/GenreDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/dal/GenreDbStorageTest.java new file mode 100644 index 0000000..2ad0e7c --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/dal/GenreDbStorageTest.java @@ -0,0 +1,87 @@ +package ru.yandex.practicum.filmorate.dal; + +import lombok.RequiredArgsConstructor; +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.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.context.annotation.Import; +import org.springframework.jdbc.core.JdbcTemplate; +import ru.yandex.practicum.filmorate.dal.mappers.GenreRowMapper; +import ru.yandex.practicum.filmorate.model.Genre; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@Import({GenreDbStorage.class, GenreRowMapper.class}) +public class GenreDbStorageTest { + + private final GenreDbStorage genreDbStorage; + private final JdbcTemplate jdbcTemplate; + + @BeforeEach + public void beforeEach() { + jdbcTemplate.execute("DELETE FROM film_genres"); + jdbcTemplate.execute("DELETE FROM films"); + // Создание тестового фильма + jdbcTemplate.update("INSERT INTO films (" + + "film_id, " + + "name, " + + "description, " + + "release_date, " + + "duration, " + + "rating_id) " + + "VALUES (1, 'Тестовый фильм', 'Описание фильма', '2025-07-14', 120, 1)"); + } + + // Тест получения списка всех жанров из БД + @Test + public void findAllGenres_returnAllGenresFromDataSql() { + Collection genres = genreDbStorage.findAllGenres(); + assertEquals(6, genres.size(), "Должно быть 6 жанров в БД"); + } + + // Тест получения жанра из БД по id + @Test + public void findGenreById_returnGenreFromDataSql() { + // Получение жанра из Optional + Optional genre = genreDbStorage.findById(1L); + assertTrue(genre.isPresent(), "Жанр с id:1 должен существовать"); + assertEquals("Комедия", genre.get().getName(), "Жанр с id:1 должен быть комедией"); + } + + // Тест получения жанров для указанного фильма + @Test + public void findGenreForFilm_returnGenresForFilm() { + // Добавляем жанры для тестового фильма с id:1 + jdbcTemplate.update("INSERT INTO film_genres (film_id, genre_id) VALUES (1, 1)"); + jdbcTemplate.update("INSERT INTO film_genres (film_id, genre_id) VALUES (1, 2)"); + + Collection genres = genreDbStorage.findGenreForFilm(1L); + assertEquals(2, genres.size(), "У фильма должно быть 2 жанра"); + assertTrue(genres.stream().anyMatch(g -> g.getId() == 1L && g.getName().equals("Комедия")), + "Должен быть жанр 'Комедия' (id=1)"); + assertTrue(genres.stream().anyMatch(g -> g.getId() == 2L && g.getName().equals("Драма")), + "Должен быть жанр 'Драма' (id=2)"); + } + + // Тест добавления жанров для фильма + @Test + public void setGenresFilm_addGenresToFilm() { + // Добавляем жанры для тестового фильма с id:1 + jdbcTemplate.update("INSERT INTO film_genres (film_id, genre_id) VALUES (1, 1)"); + jdbcTemplate.update("INSERT INTO film_genres (film_id, genre_id) VALUES (1, 2)"); + + Collection genres = genreDbStorage.findGenreForFilm(1L); + assertTrue(genres.stream().anyMatch(g -> g.getId() == 1L && g.getName().equals("Комедия")), + "Должен быть жанр 'Комедия' (id=1)"); + assertTrue(genres.stream().anyMatch(g -> g.getId() == 2L && g.getName().equals("Драма")), + "Должен быть жанр 'Драма' (id=2)"); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/dal/LikesDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/dal/LikesDbStorageTest.java new file mode 100644 index 0000000..3e74a4a --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/dal/LikesDbStorageTest.java @@ -0,0 +1,79 @@ +package ru.yandex.practicum.filmorate.dal; + +import lombok.RequiredArgsConstructor; +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.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.context.annotation.Import; +import org.springframework.jdbc.core.JdbcTemplate; +import ru.yandex.practicum.filmorate.dal.mappers.LikesRowMapper; +import ru.yandex.practicum.filmorate.model.Likes; + +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@Import({LikesDbStorage.class, LikesRowMapper.class}) +public class LikesDbStorageTest { + + private final LikesDbStorage likesDbStorage; + private final JdbcTemplate jdbcTemplate; + + @BeforeEach + public void beforeEach() { + jdbcTemplate.execute("DELETE FROM likes"); + jdbcTemplate.execute("DELETE FROM films"); + jdbcTemplate.execute("DELETE FROM users"); + + // Создание тестового фильма + jdbcTemplate.update("INSERT INTO films (" + + "film_id, " + + "name, " + + "description, " + + "release_date, " + + "duration, " + + "rating_id) " + + "VALUES (1, 'Тестовый фильм', 'Описание фильма', '2025-07-14', 120, 1)"); + + // Создание тестовых пользователей + jdbcTemplate.update("INSERT INTO users (user_id, email, login, name, birthday) VALUES " + + "(1, 'user@yandex.ru', 'Mult1k', 'Дмитрий', '1995-02-13'), " + + "(2, 'user@gmail.com.ru', 'Friend', 'Саша', '1995-04-24')"); + } + + // Тест добавления лайка фильму + @Test + public void addLike_shouldAddLike() { + likesDbStorage.addLikeToFilm(1L, 2L); + Collection likes = likesDbStorage.getLikesOnFilm(1L); + assertEquals(1, likes.size(), "У тестового фильма должен быть 1 лайк"); + } + + // Тест удаления лайка у фильма + @Test + public void deleteLike_removesLikeFromFilm() { + likesDbStorage.addLikeToFilm(1L, 2L); + likesDbStorage.removeLikeFromFilm(1L, 2L); + Collection likes = likesDbStorage.getLikesOnFilm(1L); + assertEquals(0, likes.size(), "У тестового фильма не должно быть лайков"); + } + + // Тест получения всех лайков у фильма + @Test + public void getAllLikesFromFilm_returnAllLikesFromFilm() { + likesDbStorage.addLikeToFilm(1L, 1L); + likesDbStorage.addLikeToFilm(1L, 2L); + Collection likes = likesDbStorage.getLikesOnFilm(1L); + assertEquals(2, likes.size(), "У тестового фильма должно быть 2 лайка"); + assertTrue(likes.stream().anyMatch(like -> like.getUserId() == 1L), + "Должен быть лайк от Дмитрия"); + assertTrue(likes.stream().anyMatch(like -> like.getUserId() == 2L), + "Должен быть лайк от Саши"); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/dal/MpaDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/dal/MpaDbStorageTest.java new file mode 100644 index 0000000..2c342d0 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/dal/MpaDbStorageTest.java @@ -0,0 +1,67 @@ +package ru.yandex.practicum.filmorate.dal; + +import lombok.RequiredArgsConstructor; +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.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.context.annotation.Import; +import org.springframework.jdbc.core.JdbcTemplate; +import ru.yandex.practicum.filmorate.dal.mappers.MpaRowMapper; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.util.Collection; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@Import({MpaDbStorage.class, MpaRowMapper.class}) +public class MpaDbStorageTest { + + private final MpaDbStorage mpaDbStorage; + private final JdbcTemplate jdbcTemplate; + + @BeforeEach + public void beforeEach() { + jdbcTemplate.execute("DELETE FROM films"); + // Создание тестового фильма + jdbcTemplate.update("INSERT INTO films (" + + "film_id, " + + "name, " + + "description, " + + "release_date, " + + "duration, " + + "rating_id) " + + "VALUES (1, 'Тестовый фильм', 'Описание фильма', '2025-07-14', 120, 5)"); + } + + // Тест получения всех жанров + @Test + public void findAllMpa_returnAllMpaFromDataSql() { + Collection allMpa = mpaDbStorage.findAll(); + assertEquals(5, allMpa.size(), "Должно быть 5 MPA-рейтингов в БД"); + } + + // Тест получения MPA-рейтинга из БД по id + @Test + public void findMpa_returnMpaFromDataSql() { + // Получение MPA-рейтинга из Optional + Optional mpa = mpaDbStorage.findMpaById(4L); + assertTrue(mpa.isPresent(), "Жанр с id:4 должен существовать"); + assertEquals("R", mpa.get().getName(), "Жанр с id:4 должен быть R"); + } + + // Тест получения жанра для указанного фильма + @Test + public void findMpaForFilm_returnMpaToFilm() { + Optional mpaFilm = mpaDbStorage.findMpaFilm(1L); + assertTrue(mpaFilm.isPresent(), "Фильм должен иметь рейтинг"); + assertEquals(5, mpaFilm.get().getId(), "Фильм должен иметь рейтинг с id=5"); + assertEquals("NC-17", mpaFilm.get().getName(), "Фильм должен иметь рейтинг NC-17"); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/dal/UserDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/dal/UserDbStorageTest.java new file mode 100644 index 0000000..3bef5bd --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/dal/UserDbStorageTest.java @@ -0,0 +1,129 @@ +package ru.yandex.practicum.filmorate.dal; + +import lombok.RequiredArgsConstructor; +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.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.context.annotation.Import; +import org.springframework.jdbc.core.JdbcTemplate; +import ru.yandex.practicum.filmorate.dal.mappers.*; +import ru.yandex.practicum.filmorate.model.User; + +import java.time.LocalDate; +import java.util.Collection; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@Import({UserDbStorage.class, UserRowMapper.class}) +public class UserDbStorageTest { + + private final UserDbStorage userDbStorage; + private final JdbcTemplate jdbcTemplate; + + @BeforeEach + public void beforeEach() { + // Очистка данных + jdbcTemplate.execute("DELETE FROM users"); + } + + // Тест создания пользователя с валидными данными и получения его по id + @Test + public void createUser_withValidFields_return_userAddedWithGeneratedId() { + User user = createUserTest("user@yandex.ru", "Mult1k", "Дмитрий", + LocalDate.of(1995, 2, 13)); + + // Сохранение пользователя в БД и поиск по id + User newUser = userDbStorage.create(user); + Optional userOptional = userDbStorage.findById(newUser.getId()); + assertTrue(userOptional.isPresent(), "Пользователя должен существовать"); + + // Получение пользователя из Optional + User retrievedUser = userOptional.get(); + + // Проверка основных полей пользователя + assertUserEquals(user, retrievedUser); + } + + // Тест получения списка всех созданных пользователей + @Test + public void findAll_afterAddingTwoUsers_returnsCollectionSize2() { + User user = createUserTest("user@yandex.ru", "Mult1k", "Дмитрий", + LocalDate.of(1995, 2, 13)); + User user2 = createUserTest("user@gmail.com", "Login2", "name", + LocalDate.of(1995, 4, 24)); + + // Сохранение пользователей в БД + userDbStorage.create(user); + userDbStorage.create(user2); + + Collection users = userDbStorage.findAll(); + assertEquals(2, users.size(), "Должно быть найдено 2 пользователя"); + } + + // Тест обновления пользователя + @Test + public void update_existingUserWithValidData_fieldsUpdated() { + User user = createUserTest("user@yandex.ru", "Mult1k", "Дмитрий", + LocalDate.of(1995, 2, 13)); + + // Сохранение пользователя в БД + User createdUser = userDbStorage.create(user); + + // Обновление данных пользователя + User updatedUser = createUserTest("user@gmail.com", "Login2", "name", + LocalDate.of(1995, 4, 24)); + updatedUser.setId(createdUser.getId()); + + // Обновление пользователя в БД и получение его из Optional + userDbStorage.update(updatedUser); + User retrievedUser = userDbStorage.findById(createdUser.getId()).orElseThrow(); + + // Проверка основных полей пользователя + assertEquals(updatedUser, retrievedUser); + } + + // Тест удаления пользователя по id + @Test + public void delete_existingUser_removesUser() { + User user = createUserTest("user@yandex.ru", "Mult1k", "Дмитрий", + LocalDate.of(1995, 2, 13)); + + // Сохранение пользователя в БД + userDbStorage.create(user); + + // Удаление пользователя + userDbStorage.delete(user.getId()); + Optional userOptional = userDbStorage.findById(user.getId()); + assertEquals(Optional.empty(), userOptional, "Созданный пользователя должен быть удален"); + } + + // Вспомогательный метод для создания тестового пользователя + private User createUserTest( + String email, + String login, + String name, + LocalDate birthday + ) { + User user = new User(); + user.setEmail(email); + user.setLogin(login); + user.setName(name); + user.setBirthday(birthday); + return user; + } + + // Вспомогательный метод для проверки основных полей пользователя + private void assertUserEquals(User expected, User actual) { + assertEquals(expected.getEmail(), actual.getEmail(), "Email должен совпадать"); + assertEquals(expected.getLogin(), actual.getLogin(), "Логин пользователя должен совпадать"); + assertEquals(expected.getName(), actual.getName(), "Имя пользователя должно совпадать"); + assertEquals(expected.getBirthday(), actual.getBirthday(), "Дата рождения должна совпадать"); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/service/FilmServiceTest.java similarity index 62% rename from src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java rename to src/test/java/ru/yandex/practicum/filmorate/service/FilmServiceTest.java index 2a46821..79c2c05 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/service/FilmServiceTest.java @@ -1,63 +1,93 @@ -package ru.yandex.practicum.filmorate.controller; +package ru.yandex.practicum.filmorate.service; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import ru.yandex.practicum.filmorate.controller.*; +import ru.yandex.practicum.filmorate.dto.*; import ru.yandex.practicum.filmorate.exception.*; -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.storage.film.InMemoryFilmStorage; -import ru.yandex.practicum.filmorate.storage.user.InMemoryUserStorage; -import ru.yandex.practicum.filmorate.utils.FilmValidate; +import ru.yandex.practicum.filmorate.model.*; +import ru.yandex.practicum.filmorate.storage.*; +import ru.yandex.practicum.filmorate.utils.*; import java.time.LocalDate; import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import static org.junit.jupiter.api.Assertions.*; -class FilmControllerTest { +public class FilmServiceTest { + private final Map films = new HashMap<>(); private FilmController filmController; - private InMemoryFilmStorage filmStorage; - private InMemoryUserStorage userStorage; + private FilmTestStorage filmTestStorage; + private UserTestStorage userTestStorage; private FilmValidate filmValidate; - + private GenreTestStorage genreTestStorage; + private MpaTestStorage mpaTestStorage; + private LikesTestStorage likesTestStorage; + private UserController userController; + private UserValidate userValidate; + private FriendshipTestStorage friendshipTestStorage; @BeforeEach public void beforeEach() { - filmStorage = new InMemoryFilmStorage(); - userStorage = new InMemoryUserStorage(); + films.clear(); + + filmTestStorage = new FilmTestStorage(); + userTestStorage = new UserTestStorage(); filmValidate = new FilmValidate(); + genreTestStorage = new GenreTestStorage(); + mpaTestStorage = new MpaTestStorage(); + likesTestStorage = new LikesTestStorage(); + filmController = new FilmController( - new FilmService(filmStorage, userStorage, filmValidate) + new FilmService(filmTestStorage, + userTestStorage, + filmValidate, + genreTestStorage, + mpaTestStorage, + likesTestStorage) ); + + userController = new UserController(new UserService(userTestStorage, userValidate, friendshipTestStorage)); } // Тест успешного создания фильма с валидными данными @Test public void create_allRequiredFieldsValid_filmAddedWithGeneratedId() { - Film film = new Film(); + NewFilmRequest film = new NewFilmRequest(); film.setName("Фильм 1"); film.setDescription("Описание фильма"); film.setReleaseDate(LocalDate.of(1995, 2, 13)); film.setDuration(120); - Film createdFilm = filmController.create(film); + Mpa mpa = new Mpa(); + mpa.setId(1L); + mpa.setName("G"); + film.setMpa(mpa); + + FilmDto createdFilm = filmController.create(film); assertNotNull(createdFilm.getId(), "Фильму не был присвоен id"); - assertEquals(1, filmController.findAll().size(), + assertEquals(1, filmTestStorage.findAll().size(), "Неверное количество фильмов после создания"); } // Тест граничного условия для даты релиза - релиз раньше 28.12.1895 не допустим @Test public void create_releaseDateBeforeFirstFilm_throwsValidationException() { - Film film = new Film(); + NewFilmRequest film = new NewFilmRequest(); film.setName("Фильм с некорректной датой релиза"); film.setDescription("Описание фильма"); film.setReleaseDate(LocalDate.of(1895, 12, 27)); film.setDuration(120); + Mpa mpa = new Mpa(); + mpa.setId(1L); + mpa.setName("G"); + film.setMpa(mpa); + assertThrows(ValidationException.class, () -> filmController.create(film), "Ожидалось ValidationException при дате релиза раньше 28.12.1895"); } @@ -65,24 +95,34 @@ public void create_releaseDateBeforeFirstFilm_throwsValidationException() { // Тест граничного условия для даты релиза - релиз 28.12.1895 должен быть допустим @Test public void create_releaseDateEqualsFirstFilm_noValidationErrors() { - Film film = new Film(); + NewFilmRequest film = new NewFilmRequest(); film.setName("Старый фильм"); film.setDescription("Описание"); film.setReleaseDate(LocalDate.of(1895, 12, 28)); film.setDuration(15); + Mpa mpa = new Mpa(); + mpa.setId(1L); + mpa.setName("G"); + film.setMpa(mpa); + assertDoesNotThrow(() -> filmController.create(film), "Дата релиза 28.12.1895 должна быть допустима"); } // Тест добавления фильма без даты релиза - релиз не может быть null @Test public void create_releaseDateNull_throwsValidationException() { - Film film = new Film(); + NewFilmRequest film = new NewFilmRequest(); film.setName("Фильм"); film.setDescription("Описание"); film.setReleaseDate(null); film.setDuration(15); + Mpa mpa = new Mpa(); + mpa.setId(1L); + mpa.setName("G"); + film.setMpa(mpa); + assertThrows(NullPointerException.class, () -> filmController.create(film), "Дата релиза не может быть null"); } @@ -90,13 +130,18 @@ public void create_releaseDateNull_throwsValidationException() { // Тест на обновление несуществующего фильма @Test public void update_nonExistentFilmId_throwsNotFoundException() { - Film film = new Film(); + UpdateFilmRequest film = new UpdateFilmRequest(); film.setId(14L); film.setName("Фильм"); film.setDescription("Описание фильма"); film.setReleaseDate(LocalDate.of(1995, 12, 28)); film.setDuration(120); + Mpa mpa = new Mpa(); + mpa.setId(1L); + mpa.setName("G"); + film.setMpa(mpa); + assertThrows(NotFoundException.class, () -> filmController.update(film), "Ожидалось NotFoundException при обновлении несуществующего фильма"); } @@ -104,13 +149,18 @@ public void update_nonExistentFilmId_throwsNotFoundException() { // Тест проверяет валидацию Id при обновлении - Id не должен быть null @Test public void update_nullId_throwsValidationException() { - Film film = new Film(); + UpdateFilmRequest film = new UpdateFilmRequest(); film.setId(null); film.setName("Фильма с нулевым id"); film.setDescription("Описание фильма"); film.setReleaseDate(LocalDate.of(2025, 1, 14)); film.setDuration(120); + Mpa mpa = new Mpa(); + mpa.setId(1L); + mpa.setName("G"); + film.setMpa(mpa); + assertThrows(ValidationException.class, () -> filmController.update(film), "Ожидалось ValidationException при обновлении с null ID"); } @@ -119,69 +169,105 @@ public void update_nullId_throwsValidationException() { @Test public void update_existingFilmWithValidData_fieldsUpdated() { // Создание фильма - Film film = new Film(); + NewFilmRequest film = new NewFilmRequest(); film.setName("Начало"); film.setDescription("Сон внутри сна"); film.setReleaseDate(LocalDate.of(2010, 7, 22)); film.setDuration(148); - Film createdFilm = filmController.create(film); + + Mpa mpa = new Mpa(); + mpa.setId(1L); + mpa.setName("G"); + film.setMpa(mpa); + + FilmDto createdFilm = filmController.create(film); // Обновление фильма - Film updatedFilm = new Film(); + UpdateFilmRequest updatedFilm = new UpdateFilmRequest(); updatedFilm.setId(createdFilm.getId()); updatedFilm.setName("Уже не Начало"); updatedFilm.setDescription("Просто описание"); updatedFilm.setReleaseDate(LocalDate.of(1996, 1, 1)); updatedFilm.setDuration(150); - Film result = filmController.update(updatedFilm); + + mpa.setId(2L); + mpa.setName("PG"); + updatedFilm.setMpa(mpa); + + FilmDto result = filmController.update(updatedFilm); assertEquals("Уже не Начало", result.getName(), "Название фильма не обновилось"); assertEquals("Просто описание", result.getDescription(), "Описание фильма не обновилось"); assertEquals(150, result.getDuration(), "Продолжительность фильма не обновилась"); + assertEquals("PG", result.getMpa().getName(), "MPA-рейтинг фильма не обновился"); } // Тест на получение списка всех созданных фильмов @Test public void findAll_afterAddingTwoFilms_returnsCollectionSize2() { // Создаем 2 фильма - Film film1 = new Film(); + NewFilmRequest film1 = new NewFilmRequest(); film1.setName("Брат"); film1.setDescription("Описание фильма"); film1.setReleaseDate(LocalDate.of(1997, 12, 12)); film1.setDuration(100); - filmController.create(film1); - Film film2 = new Film(); + Mpa mpa = new Mpa(); + mpa.setId(1L); + mpa.setName("G"); + film1.setMpa(mpa); + + FilmDto createdFilm = filmController.create(film1); + + NewFilmRequest film2 = new NewFilmRequest(); film2.setName("Брат 2"); film2.setDescription("Описание продолжения"); film2.setReleaseDate(LocalDate.of(2000, 5, 11)); film2.setDuration(127); - filmController.create(film2); + + Mpa mpa2 = new Mpa(); + mpa2.setId(1L); + mpa2.setName("G"); + film2.setMpa(mpa2); + + FilmDto createdFilm2 = filmController.create(film2); assertEquals(2, filmController.findAll().size(), "Неверное количество фильмов в списке"); + assertEquals("Брат", createdFilm.getName(), "Название 1-го фильма должно быть Брат"); + assertEquals("Брат 2", createdFilm2.getName(), "Название 2-го фильма должно быть Брат 2"); } // Тест граничного условия для описания - 200 символов должно быть допустимо @Test public void create_descriptionExactly200Chars_noValidationErrors() { - Film film = new Film(); + NewFilmRequest film = new NewFilmRequest(); film.setName("Фильм"); film.setDescription("A".repeat(200)); film.setReleaseDate(LocalDate.of(2025, 5, 30)); film.setDuration(95); + Mpa mpa = new Mpa(); + mpa.setId(3L); + mpa.setName("PG-13"); + film.setMpa(mpa); + assertDoesNotThrow(() -> filmController.create(film), "Описание в 200 символов должно быть допустимо"); } // Тест граничного условия описания - длина описания не может превышать 200 символов @Test public void create_description201Chars_throwsValidationException() { - Film film = new Film(); + NewFilmRequest film = new NewFilmRequest(); film.setName("Фильм"); film.setDescription("А".repeat(201)); film.setReleaseDate(LocalDate.of(1995, 12, 28)); film.setDuration(120); + Mpa mpa = new Mpa(); + mpa.setId(4L); + mpa.setName("R"); + film.setMpa(mpa); + assertThrows(ValidationException.class, () -> filmController.create(film), "Ожидалось ValidationException при описании длиной 201 символ "); } @@ -189,12 +275,17 @@ public void create_description201Chars_throwsValidationException() { // Тест граничного условия для продолжительности: минимальная продолжительность (1 минута) должна быть допустима @Test public void create_durationOne_noValidationErrors() { - Film film = new Film(); + NewFilmRequest film = new NewFilmRequest(); film.setName("Фильм"); film.setDescription("Описание"); film.setReleaseDate(LocalDate.of(2025, 5, 30)); film.setDuration(1); + Mpa mpa = new Mpa(); + mpa.setId(4L); + mpa.setName("R"); + film.setMpa(mpa); + assertDoesNotThrow(() -> filmController.create(film), "Продолжительность фильма в 1 минуту должна быть допустима"); } @@ -202,12 +293,17 @@ public void create_durationOne_noValidationErrors() { // Тест граничного условия для продолжительности - продолжительность должна быть больше 0 @Test public void create_duration0_throwsValidationException() { - Film film = new Film(); + NewFilmRequest film = new NewFilmRequest(); film.setName("Фильм"); film.setDescription("Описание"); film.setReleaseDate(LocalDate.of(1995, 12, 28)); film.setDuration(0); + Mpa mpa = new Mpa(); + mpa.setId(4L); + mpa.setName("R"); + film.setMpa(mpa); + assertThrows(ValidationException.class, () -> filmController.create(film), "Ожидалось ValidationException при продолжительности 0"); } @@ -215,12 +311,17 @@ public void create_duration0_throwsValidationException() { // Тест граничного условия для продолжительности - продолжительность не может быть меньше 0 @Test public void create_durationNegative_throwsValidationException() { - Film film = new Film(); + NewFilmRequest film = new NewFilmRequest(); film.setName("Фильм"); film.setDescription("Описание"); film.setReleaseDate(LocalDate.of(1995, 12, 28)); film.setDuration(-1); + Mpa mpa = new Mpa(); + mpa.setId(5L); + mpa.setName("NC-17"); + film.setMpa(mpa); + assertThrows(ValidationException.class, () -> filmController.create(film), "Ожидалось ValidationException при отрицательной продолжительности"); } @@ -228,12 +329,17 @@ public void create_durationNegative_throwsValidationException() { // Тест проверяет валидацию имени фильма - пустая строка недопустима @Test public void create_EmptyName_throwsValidationException() { - Film film = new Film(); + NewFilmRequest film = new NewFilmRequest(); film.setName(""); film.setDescription("Описание"); film.setReleaseDate(LocalDate.of(2024, 12, 31)); film.setDuration(160); + Mpa mpa = new Mpa(); + mpa.setId(5L); + mpa.setName("NC-17"); + film.setMpa(mpa); + assertThrows(ValidationException.class, () -> filmController.create(film), "Ожидалось ValidationException при пустом имени фильма"); } @@ -249,14 +355,19 @@ public void create_nullFilm_throwsValidationException() { // Тест успешного удаления фильма по Id @Test public void delete_existingFilm_removesFilm() { - Film film = new Film(); + NewFilmRequest film = new NewFilmRequest(); film.setName("Фильм для удаления"); film.setDescription("Описание"); film.setReleaseDate(LocalDate.of(2025, 6, 14)); film.setDuration(100); - filmController.create(film); - filmController.delete(film.getId()); + Mpa mpa = new Mpa(); + mpa.setId(5L); + mpa.setName("NC-17"); + film.setMpa(mpa); + + FilmDto filmForDelete = filmController.create(film); + filmController.delete(filmForDelete.getId()); assertEquals(0, filmController.findAll().size(), "Фильм должен быть удален"); } @@ -271,16 +382,21 @@ public void delete_nonExistentFilm_throwsNotFoundException() { // Тест получения фильма по Id @Test public void getById_existingFilm_returnsFilm() { - Film film = new Film(); + NewFilmRequest film = new NewFilmRequest(); film.setName("Фильм для поиска"); film.setDescription("Описание"); film.setReleaseDate(LocalDate.of(2025, 6, 14)); film.setDuration(100); - filmController.create(film); - Film foundFilm = filmController.getById(film.getId()); + Mpa mpa = new Mpa(); + mpa.setId(5L); + mpa.setName("NC-17"); + film.setMpa(mpa); - assertEquals(film, foundFilm, "Найденный фильм должен соответствовать созданному"); + FilmDto createdFilm = filmController.create(film); + FilmDto foundFilm = filmController.getById(createdFilm.getId()); + + assertEquals(foundFilm, createdFilm, "Найденный фильм должен соответствовать созданному"); } // Тест получения несуществующего фильма по Id @@ -290,125 +406,165 @@ public void getById_nonExistentFilm_throwsNotFoundException() { "Ожидалось NotFoundException при поиске несуществующего фильма"); } - // Тест добавления лайка фильму + // Тест добавления лайка @Test public void addLike_validUserAndFilm_addsLike() { - Film film = new Film(); + NewFilmRequest film = new NewFilmRequest(); film.setName("Фильм для лайка"); film.setDescription("Описание"); film.setReleaseDate(LocalDate.of(2025, 6, 14)); film.setDuration(100); - filmController.create(film); - User user = new User(); + Mpa mpa = new Mpa(); + mpa.setId(5L); + mpa.setName("NC-17"); + film.setMpa(mpa); + + FilmDto createdFilm = filmController.create(film); + + NewUserRequest user = new NewUserRequest(); user.setEmail("user@mail.ru"); user.setLogin("user_login"); user.setBirthday(LocalDate.of(1995, 2, 13)); - userStorage.create(user); - filmController.addLike(film.getId(), user.getId()); + UserDto createdUser = userController.create(user); + filmController.addLike(createdFilm.getId(), createdUser.getId()); - Film updatedFilm = filmController.getById(film.getId()); - assertTrue(updatedFilm.getLikesByUsers().contains(user.getId()), "Лайк должен быть добавлен"); + FilmDto updatedFilm = filmController.getById(createdFilm.getId()); + assertTrue(updatedFilm.getLikesByUsers().contains(createdUser.getId()), "Лайк должен быть добавлен"); } //Тест добавления лайка несуществующему фильму @Test public void addLike_nonExistentFilm_throwsNotFoundException() { - User user = new User(); + NewUserRequest user = new NewUserRequest(); user.setEmail("user@mail.ru"); user.setLogin("user_login"); user.setBirthday(LocalDate.of(1995, 2, 13)); - userStorage.create(user); - assertThrows(NotFoundException.class, () -> filmController.addLike(999L, user.getId()), + UserDto createdUser = userController.create(user); + + assertThrows(NotFoundException.class, () -> filmController.addLike(999L, createdUser.getId()), "Ожидалось NotFoundException при лайке несуществующего фильма"); } // Тест добавления лайка от несуществующего пользователя @Test public void addLike_nonExistentUser_throwsNotFoundException() { - Film film = new Film(); + NewFilmRequest film = new NewFilmRequest(); film.setName("Фильм для лайка"); film.setDescription("Описание"); film.setReleaseDate(LocalDate.of(2025, 6, 14)); film.setDuration(100); - filmController.create(film); - assertThrows(NotFoundException.class, () -> filmController.addLike(film.getId(), 999L), + Mpa mpa = new Mpa(); + mpa.setId(5L); + mpa.setName("NC-17"); + film.setMpa(mpa); + + FilmDto createdFilm = filmController.create(film); + + assertThrows(NotFoundException.class, () -> filmController.addLike(createdFilm.getId(), 999L), "Ожидалось NotFoundException при лайке от несуществующего пользователя"); } // Тест успешного удаления лайка @Test public void removeLike_existingLike_removesLike() { - Film film = new Film(); + NewFilmRequest film = new NewFilmRequest(); film.setName("Фильм для лайка"); film.setDescription("Описание"); film.setReleaseDate(LocalDate.of(2025, 6, 14)); film.setDuration(100); - filmController.create(film); - User user = new User(); + Mpa mpa = new Mpa(); + mpa.setId(5L); + mpa.setName("NC-17"); + film.setMpa(mpa); + + FilmDto createdFilm = filmController.create(film); + + NewUserRequest user = new NewUserRequest(); user.setEmail("user@mail.ru"); user.setLogin("user_login"); user.setBirthday(LocalDate.of(1995, 2, 13)); - userStorage.create(user); - filmController.addLike(film.getId(), user.getId()); - filmController.removeLike(film.getId(), user.getId()); + UserDto createdUser = userController.create(user); - Film updatedFilm = filmController.getById(film.getId()); - assertFalse(updatedFilm.getLikesByUsers().contains(user.getId()), "Лайк должен быть удален"); + filmController.addLike(createdFilm.getId(), createdUser.getId()); + filmController.removeLike(createdFilm.getId(), createdFilm.getId()); + + FilmDto updatedFilm = filmController.getById(createdFilm.getId()); + assertFalse(updatedFilm.getLikesByUsers().contains(createdUser.getId()), "Лайк должен быть удален"); } // Тест получения популярных фильмов @Test public void getPopularFilms_returnsFilmsOrderedByLikes() { - Film film = new Film(); + // Создаем 3 фильма + NewFilmRequest film = new NewFilmRequest(); film.setName("Фильм для 2 лайков"); film.setDescription("Описание"); film.setReleaseDate(LocalDate.of(2025, 6, 14)); film.setDuration(100); - filmController.create(film); - Film film2 = new Film(); + Mpa mpa = new Mpa(); + mpa.setId(5L); + mpa.setName("NC-17"); + film.setMpa(mpa); + + FilmDto createdFilm1 = filmController.create(film); + + NewFilmRequest film2 = new NewFilmRequest(); film2.setName("Фильм для 1 лайка"); film2.setDescription("Описание"); film2.setReleaseDate(LocalDate.of(2025, 6, 14)); film2.setDuration(120); - filmController.create(film2); - Film film3 = new Film(); + Mpa mpa2 = new Mpa(); + mpa2.setId(1L); + mpa2.setName("G"); + film2.setMpa(mpa2); + + FilmDto createdFilm2 = filmController.create(film2); + + NewFilmRequest film3 = new NewFilmRequest(); film3.setName("Фильм без лайков"); film3.setDescription("Описание"); film3.setReleaseDate(LocalDate.of(2025, 5, 14)); film3.setDuration(70); - filmController.create(film3); - User user = new User(); + Mpa mpa3 = new Mpa(); + mpa3.setId(4L); + mpa3.setName("R"); + film3.setMpa(mpa3); + + FilmDto createdFilm3 = filmController.create(film3); + + // Создаем 2 пользователей + NewUserRequest user = new NewUserRequest(); user.setEmail("user@mail.ru"); user.setLogin("user_login"); user.setBirthday(LocalDate.of(1995, 2, 13)); - userStorage.create(user); + UserDto createdUser = userController.create(user); - User user2 = new User(); + NewUserRequest user2 = new NewUserRequest(); user2.setEmail("user@yandex.ru"); user2.setLogin("user2_login"); user2.setBirthday(LocalDate.of(1996, 2, 14)); - userStorage.create(user2); + UserDto createdUser2 = userController.create(user2); - // film получает 2 лайка - filmController.addLike(film.getId(), user.getId()); - filmController.addLike(film.getId(), user2.getId()); + // createdFilm1 получает 2 лайка + filmController.addLike(createdFilm1.getId(), createdUser.getId()); + filmController.addLike(createdFilm1.getId(), createdUser2.getId()); - // film2 получает 1 лайк - filmController.addLike(film2.getId(), user2.getId()); + // createdFilm2 получает 1 лайк + filmController.addLike(createdFilm2.getId(), createdUser2.getId()); - Collection popularFilms = filmController.getPopularFilms(2); + Collection popularFilms = filmController.getPopularFilms(2); assertEquals(2, popularFilms.size(), "Должны вернуться 2 фильма"); - assertEquals(film.getId(), popularFilms.iterator().next().getId(), + assertEquals(createdFilm1.getId(), popularFilms.iterator().next().getId(), "Первый фильм должен быть самым популярным"); } } \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/service/UserServiceTest.java similarity index 61% rename from src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java rename to src/test/java/ru/yandex/practicum/filmorate/service/UserServiceTest.java index ae1ae21..853d5e4 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/service/UserServiceTest.java @@ -1,42 +1,51 @@ -package ru.yandex.practicum.filmorate.controller; +package ru.yandex.practicum.filmorate.service; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import ru.yandex.practicum.filmorate.controller.UserController; +import ru.yandex.practicum.filmorate.dto.*; import ru.yandex.practicum.filmorate.exception.*; import ru.yandex.practicum.filmorate.model.User; -import ru.yandex.practicum.filmorate.service.UserService; -import ru.yandex.practicum.filmorate.storage.user.InMemoryUserStorage; +import ru.yandex.practicum.filmorate.storage.FriendshipTestStorage; +import ru.yandex.practicum.filmorate.storage.UserTestStorage; import ru.yandex.practicum.filmorate.utils.UserValidate; import java.time.LocalDate; import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import static org.junit.jupiter.api.Assertions.*; -class UserControllerTest { +public class UserServiceTest { + + private final Map users = new HashMap<>(); private UserController userController; - private InMemoryUserStorage userStorage; + private UserTestStorage userTestStorage; private UserValidate userValidate; + private FriendshipTestStorage friendshipTestStorage; @BeforeEach public void beforeEach() { - userStorage = new InMemoryUserStorage(); + users.clear(); + + userTestStorage = new UserTestStorage(); userValidate = new UserValidate(); - userController = new UserController( - new UserService(userStorage, userValidate) - ); + friendshipTestStorage = new FriendshipTestStorage(); + + userController = new UserController(new UserService(userTestStorage, userValidate, friendshipTestStorage)); } // Тест успешного создания пользователя с валидными данными @Test public void create_allRequiredFieldsValid_userAddedWithGeneratedId() { - User user = new User(); + NewUserRequest user = new NewUserRequest(); user.setEmail("user@mail.ru"); user.setLogin("user_login"); user.setBirthday(LocalDate.of(1995, 2, 13)); - User createdUser = userController.create(user); + UserDto createdUser = userController.create(user); assertNotNull(createdUser.getId(), "Пользователю не был присвоен id"); assertEquals(1, userController.findAll().size(), @@ -46,13 +55,13 @@ public void create_allRequiredFieldsValid_userAddedWithGeneratedId() { // Тест создания пользователя с пустым именем (должно подставляться значение login) @Test public void create_emptyName_nameEqualsLogin() { - User user = new User(); + NewUserRequest user = new NewUserRequest(); user.setEmail("user@mail.ru"); user.setLogin("user_login"); user.setName(""); user.setBirthday(LocalDate.of(1995, 2, 13)); - User createdUser = userController.create(user); + UserDto createdUser = userController.create(user); assertEquals("user_login", createdUser.getName(), "При пустом имени должно подставляться значение login"); @@ -61,13 +70,13 @@ public void create_emptyName_nameEqualsLogin() { // Тест создания пользователя с null именем (должно подставляться значение login) @Test public void create_nullName_nameEqualsLogin() { - User user = new User(); + NewUserRequest user = new NewUserRequest(); user.setEmail("user@mail.ru"); user.setLogin("user_login"); user.setName(null); user.setBirthday(LocalDate.of(1995, 2, 13)); - User createdUser = userController.create(user); + UserDto createdUser = userController.create(user); assertEquals("user_login", createdUser.getName(), "При null имени должно подставляться значение login"); @@ -76,7 +85,7 @@ public void create_nullName_nameEqualsLogin() { // Тест валидации email - не может быть пустым @Test public void create_emptyEmail_throwsValidationException() { - User user = new User(); + NewUserRequest user = new NewUserRequest(); user.setEmail(""); user.setLogin("login"); user.setBirthday(LocalDate.of(1995, 2, 13)); @@ -88,7 +97,7 @@ public void create_emptyEmail_throwsValidationException() { // Тест валидации email - должен быть корректным форматом @Test public void create_invalidEmailFormat_throwsValidationException() { - User user = new User(); + NewUserRequest user = new NewUserRequest(); user.setEmail("invalid-email"); user.setLogin("login"); user.setBirthday(LocalDate.of(1995, 2, 13)); @@ -100,13 +109,13 @@ public void create_invalidEmailFormat_throwsValidationException() { // Тест уникальности email - нельзя создать пользователя с существующим email @Test public void create_duplicateEmail_throwsDuplicateException() { - User user1 = new User(); + NewUserRequest user1 = new NewUserRequest(); user1.setEmail("user@mail.ru"); user1.setLogin("login1"); user1.setBirthday(LocalDate.of(1996, 2, 14)); userController.create(user1); - User user2 = new User(); + NewUserRequest user2 = new NewUserRequest(); user2.setEmail("user@mail.ru"); user2.setLogin("login2"); user2.setBirthday(LocalDate.of(1995, 2, 13)); @@ -118,7 +127,7 @@ public void create_duplicateEmail_throwsDuplicateException() { // Тест добавления пользователя с email, равным null @Test public void create_nullEmail_throwsValidationException() { - User user = new User(); + NewUserRequest user = new NewUserRequest(); user.setEmail(null); user.setLogin("login"); user.setBirthday(LocalDate.of(1995, 2, 13)); @@ -130,7 +139,7 @@ public void create_nullEmail_throwsValidationException() { // Тест валидации логина - не может быть пустым @Test public void create_emptyLogin_throwsValidationException() { - User user = new User(); + NewUserRequest user = new NewUserRequest(); user.setEmail("user@mail.ru"); user.setLogin(""); user.setBirthday(LocalDate.of(1995, 2, 13)); @@ -142,7 +151,7 @@ public void create_emptyLogin_throwsValidationException() { // Тест валидации логина - не может содержать пробелов @Test public void create_loginWithSpaces_throwsValidationException() { - User user = new User(); + NewUserRequest user = new NewUserRequest(); user.setEmail("user@mail.ru"); user.setLogin("login with spaces"); user.setBirthday(LocalDate.of(1995, 2, 13)); @@ -154,7 +163,7 @@ public void create_loginWithSpaces_throwsValidationException() { // Тест валидации даты рождения - не может быть в будущем @Test public void create_birthdayInFuture_throwsValidationException() { - User user = new User(); + NewUserRequest user = new NewUserRequest(); user.setEmail("user@mail.ru"); user.setLogin("login"); user.setBirthday(LocalDate.now().plusDays(1)); @@ -166,7 +175,7 @@ public void create_birthdayInFuture_throwsValidationException() { // Тест обновления несуществующего пользователя @Test public void update_nonExistentUserId_throwsNotFoundException() { - User user = new User(); + UpdateUserRequest user = new UpdateUserRequest(); user.setId(999L); user.setEmail("user@mail.ru"); user.setLogin("login"); @@ -180,19 +189,19 @@ public void update_nonExistentUserId_throwsNotFoundException() { @Test public void update_existingUserWithValidData_fieldsUpdated() { // Создание пользователя - User user = new User(); + NewUserRequest user = new NewUserRequest(); user.setEmail("user@mail.ru"); user.setLogin("login"); user.setBirthday(LocalDate.of(1995, 2, 13)); - User createdUser = userController.create(user); + UserDto createdUser = userController.create(user); // Обновление пользователя - User updatedUser = new User(); + UpdateUserRequest updatedUser = new UpdateUserRequest(); updatedUser.setId(createdUser.getId()); updatedUser.setEmail("new@mail.ru"); updatedUser.setLogin("new_login"); updatedUser.setBirthday(LocalDate.of(1995, 2, 13)); - User result = userController.update(updatedUser); + UserDto result = userController.update(updatedUser); assertEquals("new@mail.ru", result.getEmail(), "Email не обновился"); assertEquals("new_login", result.getLogin(), "Логин не обновился"); @@ -203,20 +212,24 @@ public void update_existingUserWithValidData_fieldsUpdated() { // Тест получения списка всех пользователей @Test public void findAll_afterAddingTwoUsers_returnsCollectionSize2() { - User user1 = new User(); + NewUserRequest user1 = new NewUserRequest(); user1.setEmail("user1@mail.ru"); user1.setLogin("login1"); user1.setBirthday(LocalDate.of(1995, 2, 13)); - userController.create(user1); + UserDto createdUser = userController.create(user1); - User user2 = new User(); + NewUserRequest user2 = new NewUserRequest(); user2.setEmail("user2@mail.ru"); user2.setLogin("login2"); user2.setBirthday(LocalDate.of(1996, 2, 14)); - userController.create(user2); + UserDto createdUser2 = userController.create(user2); assertEquals(2, userController.findAll().size(), "Неверное количество пользователей в списке"); + assertEquals("login1", createdUser.getLogin(), + "Логин 1-го пользователя должен быть login1"); + assertEquals("login2", createdUser2.getLogin(), + "Логин 2-го пользователя должен быть login2"); } // Тест проверяет невозможность добавить пользователя равного null @@ -230,13 +243,14 @@ public void create_nullUser_throwsValidationException() { // Тест успешного удаления пользователя @Test public void delete_existingUser_removesUser() { - User user = new User(); + NewUserRequest user = new NewUserRequest(); user.setEmail("user@mail.ru"); user.setLogin("user_login"); user.setBirthday(LocalDate.of(1995, 2, 13)); - userController.create(user); - userController.delete(user.getId()); + UserDto userForDelete = userController.create(user); + userController.delete(userForDelete.getId()); + assertEquals(0, userController.findAll().size(), "Пользователь должен быть удален"); } @@ -250,15 +264,15 @@ public void delete_nonExistentUser_throwsNotFoundException() { // Тест получения пользователя по Id @Test public void getById_existingUser_returnsFilm() { - User user = new User(); + NewUserRequest user = new NewUserRequest(); user.setEmail("user@mail.ru"); user.setLogin("user_login"); user.setBirthday(LocalDate.of(1995, 2, 13)); - userController.create(user); - User foundUser = userController.getById(user.getId()); + UserDto createdUser = userController.create(user); + UserDto foundUser = userController.getById(createdUser.getId()); - assertEquals(user, foundUser, "Найденный пользователь должен соответствовать созданному"); + assertEquals(foundUser, createdUser, "Найденный пользователь должен соответствовать созданному"); } // Тест получения несуществующего пользователя @@ -268,146 +282,156 @@ public void getById_nonExistentUser_throwsNotFoundException() { "Ожидалось NotFoundException при поиске несуществующего пользователя"); } - // Тест добавления в друзья + // Тест успешного добавления в друзья @Test public void addFriend_validUserAndFriend() { - User user = new User(); + // Создаем основного пользователя + NewUserRequest user = new NewUserRequest(); user.setEmail("user@mail.ru"); user.setLogin("user_login"); user.setBirthday(LocalDate.of(1995, 2, 13)); - userController.create(user); + UserDto createdUser = userController.create(user); - User friend = new User(); + // Создаем друга + NewUserRequest friend = new NewUserRequest(); friend.setEmail("user@yandex.ru"); friend.setLogin("friend_login"); friend.setBirthday(LocalDate.of(1996, 2, 14)); - userController.create(friend); - - userController.addFriend(user.getId(), friend.getId()); - - User updatedUser = userController.getById(user.getId()); - User updatedFriend = userController.getById(friend.getId()); - assertAll( - () -> assertTrue(updatedUser.getFriends().contains(friend.getId()), - "Друг должен быть в списке пользователя"), - () -> assertTrue(updatedFriend.getFriends().contains(user.getId()), - "Пользователь должен быть в списке друга"), - () -> assertEquals(1, updatedUser.getFriends().size(), - "Должен быть ровно один друг"), - () -> assertEquals(1, updatedFriend.getFriends().size(), - "Должен быть ровно один друг") - ); + UserDto userForFriend = userController.create(friend); + + userController.addFriend(createdUser.getId(), userForFriend.getId()); + UserDto updatedUser = userController.getById(createdUser.getId()); + + assertTrue(updatedUser.getFriends().contains(userForFriend.getId()), + "Друг должен быть в списке пользователя"); + assertEquals(1, updatedUser.getFriends().size(), + "У пользователя должен быть ровно один друг"); + } + + // Тест, что дружба является односторонней + @Test + public void addFriend_shouldCreateOneSidedFriendship() { + // Создаем основного пользователя + NewUserRequest user = new NewUserRequest(); + user.setEmail("user@mail.ru"); + user.setLogin("user_login"); + user.setBirthday(LocalDate.of(1995, 2, 13)); + UserDto createdUser = userController.create(user); + + // Создаем друга + NewUserRequest friend = new NewUserRequest(); + friend.setEmail("user@yandex.ru"); + friend.setLogin("friend_login"); + friend.setBirthday(LocalDate.of(1996, 2, 14)); + UserDto userForFriend = userController.create(friend); + + userController.addFriend(createdUser.getId(), userForFriend.getId()); + UserDto updatedUser = userController.getById(createdUser.getId()); + UserDto updatedFriend = userController.getById(userForFriend.getId()); + + // Проверка, что у пользователя появился друг + assertTrue(updatedUser.getFriends().contains(userForFriend.getId()), + "Друг должен быть в списке друзей пользователя"); + + // Проверка, что у друга нет пользователя в друзьях + assertFalse(updatedFriend.getFriends().contains(createdUser.getId()), + "У друга не должно быть пользователя в друзьях"); + assertEquals(0, updatedFriend.getFriends().size(), + "У друга не должно быть друзей"); } // Тест на добавление несуществующего друга @Test public void addFriend_nonExistentFriend_throwsNotFounderException() { - User user = new User(); + NewUserRequest user = new NewUserRequest(); user.setEmail("user@mail.ru"); user.setLogin("user_login"); user.setBirthday(LocalDate.of(1995, 2, 13)); - userController.create(user); + UserDto createdUser = userController.create(user); - assertThrows(NotFoundException.class, () -> userController.addFriend(user.getId(), 999L), + assertThrows(NotFoundException.class, () -> userController.addFriend(createdUser.getId(), 999L), "Ожидалось NotFoundException при добавлении в друзья несуществующего пользователя"); } // Тест успешного удаления друга @Test public void deleteFriend_existingFriend_removesFriend() { - User user = new User(); + NewUserRequest user = new NewUserRequest(); user.setEmail("user@mail.ru"); user.setLogin("user_login"); user.setBirthday(LocalDate.of(1995, 2, 13)); - userController.create(user); + UserDto createdUser = userController.create(user); - User friend = new User(); + NewUserRequest friend = new NewUserRequest(); friend.setEmail("user@yandex.ru"); friend.setLogin("friend_login"); friend.setBirthday(LocalDate.of(1996, 2, 14)); - userController.create(friend); - - // добавляем друга и проверяем, что дружба взаимна у двух пользователей - userController.addFriend(user.getId(), friend.getId()); - User updatedUser = userController.getById(user.getId()); - User updatedFriend = userController.getById(friend.getId()); - - assertAll( - () -> assertTrue(updatedUser.getFriends().contains(friend.getId()), - "Друг должен быть в списке пользователя"), - () -> assertTrue(updatedFriend.getFriends().contains(user.getId()), - "Пользователь должен быть в списке друга"), - () -> assertEquals(1, updatedUser.getFriends().size(), - "Должен быть ровно один друг"), - () -> assertEquals(1, updatedFriend.getFriends().size(), - "Должен быть ровно один друг") - ); + UserDto friendForDelete = userController.create(friend); - // удаляем друга - userController.removeFriend(updatedUser.getId(), updatedFriend.getId()); - // Получаем обновленные данные - User userAfterRemove = userController.getById(user.getId()); - User friendAfterRemove = userController.getById(friend.getId()); + // добавляем друга и проверяем, что у пользователя появился друг + userController.addFriend(createdUser.getId(), friendForDelete.getId()); + UserDto userWithFriend = userController.getById(createdUser.getId()); + + assertTrue(userWithFriend.getFriends().contains(friendForDelete.getId()), + "Друг должен быть в списке друзей пользователя"); + + // удаляем друга и получаем обновленные данные + userController.removeFriend(createdUser.getId(), friendForDelete.getId()); + UserDto userAfterRemove = userController.getById(createdUser.getId()); // проверяем, что дружба удалилась у обоих - assertAll( - () -> assertFalse(userAfterRemove.getFriends().contains(friend.getId()), - "Друг должен быть удален из списка пользователя"), - () -> assertFalse(friendAfterRemove.getFriends().contains(user.getId()), - "Пользователь должен быть удален из списка друга"), - () -> assertEquals(0, userAfterRemove.getFriends().size(), - "Список друзей пользователя должен быть пустым"), - () -> assertEquals(0, friendAfterRemove.getFriends().size(), - "Список друзей друга должен быть пустым") - ); + assertFalse(userAfterRemove.getFriends().contains(friendForDelete.getId()), + "Друг должен быть удален из списка пользователя"); + assertEquals(0, userAfterRemove.getFriends().size(), + "Список друзей пользователя должен быть пустым"); } // Тест на получение всех друзей @Test public void getFriends_returnsAllFriends() { - User user = new User(); + NewUserRequest user = new NewUserRequest(); user.setEmail("user@mail.ru"); user.setLogin("user_login"); user.setBirthday(LocalDate.of(1995, 2, 13)); - userController.create(user); + UserDto createdUser = userController.create(user); - User friend = new User(); + NewUserRequest friend = new NewUserRequest(); friend.setEmail("user@yandex.ru"); friend.setLogin("friend_login"); friend.setBirthday(LocalDate.of(1996, 2, 14)); - userController.create(friend); + UserDto userForFriend = userController.create(friend); - User friend2 = new User(); + NewUserRequest friend2 = new NewUserRequest(); friend2.setEmail("friend@gmail.com"); friend2.setLogin("Mult1k3"); friend2.setBirthday(LocalDate.of(1988, 2, 28)); - userController.create(friend2); + UserDto userForFriend2 = userController.create(friend2); // добавляем пользователей в друзья - userController.addFriend(user.getId(), friend.getId()); - userController.addFriend(user.getId(), friend2.getId()); - User updatedUser = userController.getById(user.getId()); + userController.addFriend(createdUser.getId(), userForFriend.getId()); + userController.addFriend(createdUser.getId(), userForFriend2.getId()); + UserDto updatedUser = userController.getById(createdUser.getId()); // Получаем список друзей - Collection friends = userController.getFriends(updatedUser.getId()); + Collection friends = userController.getFriends(updatedUser.getId()); assertEquals(2, friends.size(), "Должно быть 2 друга"); - assertTrue(friends.stream().anyMatch(f -> f.getId().equals(friend.getId())), + assertTrue(friends.stream().anyMatch(f -> f.getId().equals(userForFriend.getId())), "Друг 1 должен быть в списке"); - assertTrue(friends.stream().anyMatch(f -> f.getId().equals(friend2.getId())), + assertTrue(friends.stream().anyMatch(f -> f.getId().equals(userForFriend2.getId())), "Друг 2 должен быть в списке"); } // Тест на получение пустого списка, если у пользователя нет друзей @Test public void getFriends_userWithNoFriends_returnsEmptyList() { - User user = new User(); + NewUserRequest user = new NewUserRequest(); user.setEmail("user@mail.ru"); user.setLogin("user_login"); user.setBirthday(LocalDate.of(1995, 2, 13)); - userController.create(user); + UserDto createdUser = userController.create(user); - Collection friends = userController.getFriends(user.getId()); + Collection friends = userController.getFriends(createdUser.getId()); assertTrue(friends.isEmpty(), "Список друзей должен быть пустым"); } @@ -415,34 +439,34 @@ public void getFriends_userWithNoFriends_returnsEmptyList() { // Тест на получение списка общих друзей у двух пользователей @Test public void getCommonFriends_returnsCommonFriends() { - User user = new User(); + NewUserRequest user = new NewUserRequest(); user.setEmail("user@mail.ru"); user.setLogin("user_login"); user.setBirthday(LocalDate.of(1995, 2, 13)); - userController.create(user); + UserDto createdUser = userController.create(user); - User user2 = new User(); + NewUserRequest user2 = new NewUserRequest(); user2.setEmail("user@yandex.ru"); user2.setLogin("friend_login"); user2.setBirthday(LocalDate.of(1996, 2, 14)); - userController.create(user2); + UserDto createdUser2 = userController.create(user2); - User commonFriend = new User(); - commonFriend.setEmail("friend@gmail.com"); - commonFriend.setLogin("Mult1k3"); - commonFriend.setBirthday(LocalDate.of(1988, 2, 28)); - userController.create(commonFriend); + NewUserRequest request = new NewUserRequest(); + request.setEmail("friend@gmail.com"); + request.setLogin("Mult1k3"); + request.setBirthday(LocalDate.of(1988, 2, 28)); + UserDto commonFriend = userController.create(request); // добавляем общего друга - userController.addFriend(user.getId(), commonFriend.getId()); - userController.addFriend(user2.getId(), commonFriend.getId()); + userController.addFriend(createdUser.getId(), commonFriend.getId()); + userController.addFriend(createdUser2.getId(), commonFriend.getId()); // обновляем данные - User updatedUser = userController.getById(user.getId()); - User updatedUser2 = userController.getById(user2.getId()); + UserDto updatedUser = userController.getById(createdUser.getId()); + UserDto updatedUser2 = userController.getById(createdUser2.getId()); // Проверяем общих друзей - Collection commonFriends = userController.getCommonFriends( + Collection commonFriends = userController.getCommonFriends( updatedUser.getId(), updatedUser2.getId() ); diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java b/src/test/java/ru/yandex/practicum/filmorate/storage/FilmTestStorage.java similarity index 72% rename from src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java rename to src/test/java/ru/yandex/practicum/filmorate/storage/FilmTestStorage.java index 6ad50a0..9138ec9 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/FilmTestStorage.java @@ -1,25 +1,31 @@ -package ru.yandex.practicum.filmorate.storage.film; +package ru.yandex.practicum.filmorate.storage; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Repository; import ru.yandex.practicum.filmorate.exception.*; import ru.yandex.practicum.filmorate.model.Film; import java.util.*; +import java.util.stream.Collectors; import static ru.yandex.practicum.filmorate.utils.ControllersUtils.getNextId; +/** + * Утилитарный класс - тестовое хранилище для Film + */ + @Slf4j -@Component -public class InMemoryFilmStorage implements FilmStorage { +@Repository +@Primary +public class FilmTestStorage implements FilmStorage { private final Map films = new HashMap<>(); private final Set filmNames = new HashSet<>(); - // Добавление фильма + // Создание фильма @Override public Film create(Film film) { - checkFilmNamesUniqueness(film.getName()); filmNames.add(film.getName().toLowerCase()); film.setId(getNextId(films.keySet())); @@ -32,12 +38,11 @@ public Film create(Film film) { @Override public void delete(Long id) { validateFilmId(id); - filmNames.remove(films.get(id).getName().toLowerCase()); films.remove(id); } - // Изменение фильма + // Обновление фильма @Override public Film update(Film film) { validateFilmId(film.getId()); @@ -59,15 +64,25 @@ public Film update(Film film) { // Получение фильма по id @Override - public Film getById(Long id) { + public Optional findById(Long id) { validateFilmId(id); - return films.get(id); + return Optional.ofNullable(films.get(id)); } // Получение всех фильмов @Override public Collection findAll() { - return List.copyOf(films.values()); + return films.values(); + } + + // Получения списка популярных фильмов + @Override + public Collection findPopularFilms(int count) { + return films.values().stream() + .sorted((f1, f2) -> Integer.compare(f2.getLikesByUsers().size(), + f1.getLikesByUsers().size())) + .limit(count) + .collect(Collectors.toList()); } // Вспомогательный метод для проверки на наличие дубликата названия фильма diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/FriendshipTestStorage.java b/src/test/java/ru/yandex/practicum/filmorate/storage/FriendshipTestStorage.java new file mode 100644 index 0000000..e3e774e --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/FriendshipTestStorage.java @@ -0,0 +1,68 @@ +package ru.yandex.practicum.filmorate.storage; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.User; + +import java.util.*; + +/** + * Утилитарный класс - тестовое хранилище Friendship + */ + +@Slf4j +@Repository +@Primary +public class FriendshipTestStorage implements FriendshipStorage { + + private final Map> friendships = new HashMap<>(); + private UserTestStorage userTestStorage; + + @Override + public void addFriend(Long userId, Long friendId) { + if (!friendships.containsKey(userId)) { + friendships.put(userId, new HashSet<>()); + } + friendships.get(userId).add(friendId); + } + + @Override + public void removeFriend(Long userId, Long friendId) { + // Получаем множество друзей пользователя + Set userFriends = friendships.get(userId); + + // Если множество существует — удаляем друга + if (userFriends != null) { + userFriends.remove(friendId); + } + } + + @Override + public Collection findAllFriends(Long userId) { + Set friendIds = friendships.getOrDefault(userId, Set.of()); + List friends = new ArrayList<>(); + + for (Long friendId : friendIds) { + userTestStorage.findById(friendId).ifPresent(friends::add); + } + + return friends; + } + + @Override + public Collection findCommonFriends(Long userId, Long otherId) { + Set userFriends = friendships.getOrDefault(userId, Set.of()); + Set otherFriends = friendships.getOrDefault(otherId, Set.of()); + List commonFriends = new ArrayList<>(); + + // Пересечение множеств + for (Long friendId : userFriends) { + if (otherFriends.contains(friendId)) { + userTestStorage.findById(friendId).ifPresent(commonFriends::add); + } + } + + return commonFriends; + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/GenreTestStorage.java b/src/test/java/ru/yandex/practicum/filmorate/storage/GenreTestStorage.java new file mode 100644 index 0000000..30fff9d --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/GenreTestStorage.java @@ -0,0 +1,60 @@ +package ru.yandex.practicum.filmorate.storage; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.Genre; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Утилитарный класс - тестовое хранилище Genre + */ + +@Slf4j +@Repository +@Primary +public class GenreTestStorage implements GenreStorage { + private final Map genres = new HashMap() {{ + Genre genre1 = new Genre(); + genre1.setId(1L); + genre1.setName("Комедия"); + put(1L, genre1); + + Genre genre2 = new Genre(); + genre2.setId(2L); + genre2.setName("Драма"); + put(2L, genre2); + }}; + + // Хранение связей жанров и фильма + private final Map> filmToGenres = new HashMap<>(); + + @Override + public Collection findAllGenres() { + return new ArrayList<>(genres.values()); + } + + @Override + public Optional findById(Long id) { + return Optional.ofNullable(genres.get(id)); + } + + @Override + public Collection findGenreForFilm(Long filmId) { + return filmToGenres.getOrDefault(filmId, Set.of()) + .stream() + .map(genres::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + @Override + public void setGenreForFilm(Long filmId, Collection genres) { + Set genreIds = genres.stream() + .map(Genre::getId) + .collect(Collectors.toSet()); + filmToGenres.put(filmId, genreIds); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/LikesTestStorage.java b/src/test/java/ru/yandex/practicum/filmorate/storage/LikesTestStorage.java new file mode 100644 index 0000000..250801d --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/LikesTestStorage.java @@ -0,0 +1,44 @@ +package ru.yandex.practicum.filmorate.storage; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.Likes; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Утилитарный класс - тестовое хранилище Likes + */ + +@Slf4j +@Repository +@Primary +public class LikesTestStorage implements LikesStorage { + + private final Map> filmLikes = new HashMap<>(); + + @Override + public void addLikeToFilm(Long filmId, Long userId) { + filmLikes.computeIfAbsent(filmId, k -> new HashSet<>()).add(userId); + } + + @Override + public void removeLikeFromFilm(Long filmId, Long userId) { + filmLikes.getOrDefault(filmId, Set.of()).remove(userId); + } + + @Override + public Collection getLikesOnFilm(Long filmId) { + return filmLikes.getOrDefault(filmId, Set.of()) + .stream() + .map(userId -> { + Likes like = new Likes(); + like.setFilmId(filmId); + like.setUserId(userId); + return like; + }) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/MpaTestStorage.java b/src/test/java/ru/yandex/practicum/filmorate/storage/MpaTestStorage.java new file mode 100644 index 0000000..048a3ce --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/MpaTestStorage.java @@ -0,0 +1,54 @@ +package ru.yandex.practicum.filmorate.storage; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.util.*; + +/** + * Утилитарный класс - тестовое хранилище Mpa + */ + +@Slf4j +@Repository +@Primary +public class MpaTestStorage implements MpaStorage { + + // Добавление MPA-рейтингов в тестовое хранилище Mpa + private final Map mpas = new HashMap() {{ + put(1L, createMpa(1L, "G")); + put(2L, createMpa(2L, "PG")); + put(3L, createMpa(3L, "PG-13")); + put(4L, createMpa(4L, "R")); + put(5L, createMpa(5L, "NC-17")); + }}; + + // Хранение связей фильмов и MPA + private final Map filmToMpa = new HashMap<>(); + + @Override + public Collection findAll() { + return mpas.values(); + } + + @Override + public Optional findMpaById(Long id) { + return Optional.ofNullable(mpas.get(id)); + } + + @Override + public Optional findMpaFilm(Long filmId) { + return Optional.ofNullable(filmToMpa.get(filmId)) + .flatMap(this::findMpaById); + } + + // Вспомогательный метод для добавления рейтингов + private Mpa createMpa(Long id, String name) { + Mpa mpa = new Mpa(); + mpa.setId(id); + mpa.setName(name); + return mpa; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/test/java/ru/yandex/practicum/filmorate/storage/UserTestStorage.java similarity index 87% rename from src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java rename to src/test/java/ru/yandex/practicum/filmorate/storage/UserTestStorage.java index 6eba877..eccd271 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/UserTestStorage.java @@ -1,7 +1,8 @@ -package ru.yandex.practicum.filmorate.storage.user; +package ru.yandex.practicum.filmorate.storage; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Repository; import ru.yandex.practicum.filmorate.exception.*; import ru.yandex.practicum.filmorate.model.User; @@ -9,9 +10,14 @@ import static ru.yandex.practicum.filmorate.utils.ControllersUtils.getNextId; +/** + * Утилитарный класс - тестовое хранилище для User + */ + @Slf4j -@Component -public class InMemoryUserStorage implements UserStorage { +@Repository +@Primary +public class UserTestStorage implements UserStorage { private final Map users = new HashMap<>(); private final Set usersEmail = new HashSet<>(); @@ -62,9 +68,9 @@ public User update(User user) { // Получение пользователя по id @Override - public User getById(Long id) { + public Optional findById(Long id) { validateUserId(id); - return users.get(id); + return Optional.ofNullable(users.get(id)); } // Получение всех пользователей