From cb1b0840e586e928833c37130ab4a747a0d5de6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=9A=D0=B0?= =?UTF-8?q?=D0=BD?= Date: Wed, 2 Jul 2025 15:48:05 +0300 Subject: [PATCH 1/6] =?UTF-8?q?fix:=D0=92=D0=BD=D0=B5=D1=81=D1=82=D0=B8=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=B2=20ER-=D0=B4=D0=B8?= =?UTF-8?q?=D0=B0=D0=B3=D1=80=D0=B0=D0=BC=D0=BC=D1=83=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D0=B8=D1=82=D0=BE=D0=B3=D0=B0=D0=BC=20p2p=20=D1=80=D0=B5=D0=B2?= =?UTF-8?q?=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 70 ++++++++++++++++++++++++++++++++++++++++-- filmorate_diagram.png | Bin 0 -> 64445 bytes 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 filmorate_diagram.png diff --git a/README.md b/README.md index 2cf454a..92a58b3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,68 @@ -# java-filmorate -Template repository for Filmorate project. +# Filmorate +Учебный проект с курса "Java-разработчик" от Яндекс Практикума. +Проект является backend-сервисом для получения топа фильмов, рекомендованных к просмотру на основе пользовательских оценок.
+**Стек: Java 21, Spring Boot, Apache Maven, REST, Postman, Lombok, JUnit 5** + +### Содержание +- [Этапы работы над проектом](#этапы-работы-над-проектом) +- [ER-диаграмма](#er-диаграмма) +- [Примеры запросов к базе данных](#примеры-запросов-к-базе-данных) + +## Текущий этап работы над проектом: +Спринт 12: +- Проектирование будущей базы данных; +- Создание ER-диаграммы базы данных. + +## Этапы работы над проектом +1. Спринт 10: + - Создание каркаса приложения с использользованием Spring Boot; + - Вся логика и хранение данных находятся в контроллерах; + - Разработан базовый функционал: добавление, обновление и получение всех фильмов/пользователей; + - Написаны unit-тесты для текущего функционала. +2. Спринт 11: + - Добавлены классы-хранилища для работы с данными о фильмах и пользователях; + - Добавлен новый функционал, позволяющий добавлять друзей и ставить лайки фильмам; + - Добавлены аннотации @service и @component для внедрения зависимостей; + - В классы контроллеры добавлена аннотация @ResponseStatus для указания HTTP-статуса ответа, который должен вернуться при обработке запроса; + - Добавлены классы ErrorHandler и ErrorResponse для централизованной обработки ошибок; + - Добавлены unit-тесты для нового функционала. + +## ER-диаграмма +![Схема](filmorate_diagram.png) + +## Примеры запросов к базе данных + +1. Получить всех пользователей, кто родился не раньше 1995 года +```sql +SELECT * +FROM users AS u +WHERE EXTRACT(YEAR + FROM birthday) >= 1995 +ORDER BY birthday +``` +2. Получить фильмы, у которых не меньше двух лайков +```sql +SELECT f.film_id, + f.name AS film_name, + f.description, + f.release_date, + 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 +HAVING COUNT(l.user_id) >= 2 +ORDER BY likes_count DESC; +``` + +3. Получить пользователей, у которых яндекс почта +```sql +SELECT u.user_id, + u.email, + u.login, + u.name +FROM users AS u +WHERE email LIKE '%yandex%' +``` \ No newline at end of file diff --git a/filmorate_diagram.png b/filmorate_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..d42f70d3becce12db5912348b9bd359d7dea36af GIT binary patch literal 64445 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 literal 0 HcmV?d00001 From cce36a771f081cc360c1ae4ca86d2bf96e9f62db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=9A=D0=B0?= =?UTF-8?q?=D0=BD?= Date: Tue, 15 Jul 2025 18:43:24 +0300 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=D0=92=D1=8B=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=BD=D0=B8=D1=82=D1=8C=20=D0=A4=D0=97-12.=20=D0=98=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B8=D1=82=D1=8C=20=D0=B0=D1=80=D1=85=D0=B8?= =?UTF-8?q?=D1=82=D0=B5=D0=BA=D1=82=D1=83=D1=80=D1=83=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=B0=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=91=D0=94.=20=D0=94?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D0=B8=D0=BD=D1=82?= =?UTF-8?q?=D0=B5=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=BE=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 26 +- filmorate_diagram.png | Bin 64445 -> 65560 bytes pom.xml | 114 ++++--- .../filmorate/controller/FilmController.java | 26 +- .../filmorate/controller/GenreController.java | 33 ++ .../filmorate/controller/MpaController.java | 33 ++ .../filmorate/controller/UserController.java | 28 +- .../filmorate/dal/BaseDbStorage.java | 64 ++++ .../filmorate/dal/FilmDbStorage.java | 158 +++++++++ .../filmorate/dal/FriendshipDbStorage.java | 66 ++++ .../filmorate/dal/GenreDbStorage.java | 68 ++++ .../filmorate/dal/LikesDbStorage.java | 41 +++ .../practicum/filmorate/dal/MpaDbStorage.java | 44 +++ .../filmorate/dal/UserDbStorage.java | 100 ++++++ .../filmorate/dal/mappers/FilmRowMapper.java | 35 ++ .../dal/mappers/FriendshipRowMapper.java | 20 ++ .../filmorate/dal/mappers/GenreRowMapper.java | 20 ++ .../filmorate/dal/mappers/LikesRowMapper.java | 20 ++ .../filmorate/dal/mappers/MpaRowMapper.java | 20 ++ .../filmorate/dal/mappers/UserRowMapper.java | 27 ++ .../practicum/filmorate/dto/FilmDto.java | 24 ++ .../filmorate/dto/NewFilmRequest.java | 21 ++ .../filmorate/dto/NewUserRequest.java | 18 + .../filmorate/dto/UpdateFilmRequest.java | 24 ++ .../filmorate/dto/UpdateUserRequest.java | 22 ++ .../practicum/filmorate/dto/UserDto.java | 24 ++ .../exception/InternalServerException.java | 8 + .../filmorate/mapper/FilmMapper.java | 93 +++++ .../filmorate/mapper/UserMapper.java | 67 ++++ .../practicum/filmorate/model/Film.java | 8 +- .../practicum/filmorate/model/Friendship.java | 10 + .../practicum/filmorate/model/Genre.java | 12 + .../practicum/filmorate/model/Likes.java | 9 + .../yandex/practicum/filmorate/model/Mpa.java | 12 + .../practicum/filmorate/model/User.java | 6 +- .../filmorate/service/FilmService.java | 150 +++++--- .../filmorate/service/GenreService.java | 30 ++ .../filmorate/service/MpaService.java | 30 ++ .../filmorate/service/UserService.java | 173 +++++----- .../storage/{film => }/FilmStorage.java | 9 +- .../filmorate/storage/FriendshipStorage.java | 16 + .../filmorate/storage/GenreStorage.java | 17 + .../filmorate/storage/LikesStorage.java | 14 + .../filmorate/storage/MpaStorage.java | 15 + .../storage/{user => }/UserStorage.java | 7 +- .../filmorate/utils/FilmValidate.java | 5 + .../filmorate/utils/UserValidate.java | 5 + src/main/resources/application.properties | 7 +- src/main/resources/data.sql | 16 + src/main/resources/schema.sql | 51 +++ .../filmorate/FilmorateApplicationTests.java | 4 +- .../filmorate/dal/FilmDbStorageTest.java | 224 ++++++++++++ .../dal/FriendshipDbStorageTest.java | 86 +++++ .../filmorate/dal/GenreDbStorageTest.java | 87 +++++ .../filmorate/dal/LikesDbStorageTest.java | 79 +++++ .../filmorate/dal/MpaDbStorageTest.java | 67 ++++ .../filmorate/dal/UserDbStorageTest.java | 129 +++++++ .../FilmServiceTest.java} | 320 +++++++++++++----- .../UserServiceTest.java} | 276 ++++++++------- .../filmorate/storage/FilmTestStorage.java} | 37 +- .../storage/FriendshipTestStorage.java | 68 ++++ .../filmorate/storage/GenreTestStorage.java | 60 ++++ .../filmorate/storage/LikesTestStorage.java | 44 +++ .../filmorate/storage/MpaTestStorage.java | 54 +++ .../filmorate/storage/UserTestStorage.java} | 18 +- 65 files changed, 2944 insertions(+), 455 deletions(-) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dal/BaseDbStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dal/FilmDbStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dal/FriendshipDbStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dal/GenreDbStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dal/LikesDbStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dal/MpaDbStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dal/UserDbStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dal/mappers/FilmRowMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dal/mappers/FriendshipRowMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dal/mappers/GenreRowMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dal/mappers/LikesRowMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dal/mappers/MpaRowMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dal/mappers/UserRowMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/NewFilmRequest.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/NewUserRequest.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/UpdateFilmRequest.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/UpdateUserRequest.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/UserDto.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/exception/InternalServerException.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/mapper/UserMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/Friendship.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/Genre.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/Likes.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/Mpa.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java rename src/main/java/ru/yandex/practicum/filmorate/storage/{film => }/FilmStorage.java (51%) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/FriendshipStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/GenreStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/LikesStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/MpaStorage.java rename src/main/java/ru/yandex/practicum/filmorate/storage/{user => }/UserStorage.java (58%) create mode 100644 src/main/resources/data.sql create mode 100644 src/main/resources/schema.sql create mode 100644 src/test/java/ru/yandex/practicum/filmorate/dal/FilmDbStorageTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/dal/FriendshipDbStorageTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/dal/GenreDbStorageTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/dal/LikesDbStorageTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/dal/MpaDbStorageTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/dal/UserDbStorageTest.java rename src/test/java/ru/yandex/practicum/filmorate/{controller/FilmControllerTest.java => service/FilmServiceTest.java} (62%) rename src/test/java/ru/yandex/practicum/filmorate/{controller/UserControllerTest.java => service/UserServiceTest.java} (61%) rename src/{main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java => test/java/ru/yandex/practicum/filmorate/storage/FilmTestStorage.java} (72%) create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/FriendshipTestStorage.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/GenreTestStorage.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/LikesTestStorage.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/MpaTestStorage.java rename src/{main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java => test/java/ru/yandex/practicum/filmorate/storage/UserTestStorage.java} (87%) diff --git a/README.md b/README.md index 92a58b3..493a848 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,13 @@ ## Текущий этап работы над проектом: Спринт 12: -- Проектирование будущей базы данных; -- Создание ER-диаграммы базы данных. +- Переработана архитектура проекта; +- Добавлены новые модели: MPA-рейтинг, Genre, Likes, Friendship; +- Добавлен слой DAL для работы с данными из базы данных; +- Добавлен маппинг для преобразования записей из базы данных в объекты и обратно; +- Написаны интеграционные тесты; +- Добавлены тестовые хранилища для unit-тестов; +- Переработаны unit-тесты для граничных условий с учетом новой архитектуры проекта; ## Этапы работы над проектом 1. Спринт 10: @@ -26,6 +31,9 @@ - В классы контроллеры добавлена аннотация @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,16 +51,15 @@ 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; ``` @@ -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 d42f70d3becce12db5912348b9bd359d7dea36af..68b563dedb7075ea8042e3de014c81ad8b266564 100644 GIT binary patch literal 65560 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 literal 64445 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 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..2dac69f 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -4,6 +4,10 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.dto.NewUserRequest; +import ru.yandex.practicum.filmorate.dto.UpdateFilmRequest; +import ru.yandex.practicum.filmorate.dto.UpdateUserRequest; +import ru.yandex.practicum.filmorate.dto.UserDto; import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.service.UserService; @@ -18,22 +22,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 +48,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 +68,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..1f170b7 --- /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..400770b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/GenreDbStorage.java @@ -0,0 +1,68 @@ +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.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@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..364832a --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/UserDbStorage.java @@ -0,0 +1,100 @@ +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)); } // Получение всех пользователей From 87c763ea45fe6ac233ed9e0f8fe1b88a29b9e78e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=9A=D0=B0?= =?UTF-8?q?=D0=BD?= Date: Tue, 15 Jul 2025 20:03:52 +0300 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20CodeStyle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yandex/practicum/filmorate/controller/UserController.java | 2 -- 1 file changed, 2 deletions(-) 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 2dac69f..80e262f 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -5,10 +5,8 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import ru.yandex.practicum.filmorate.dto.NewUserRequest; -import ru.yandex.practicum.filmorate.dto.UpdateFilmRequest; import ru.yandex.practicum.filmorate.dto.UpdateUserRequest; import ru.yandex.practicum.filmorate.dto.UserDto; -import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.service.UserService; import java.util.*; From 2909cd1a1af4e41b85235987f354d185efd907ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=9A=D0=B0?= =?UTF-8?q?=D0=BD?= Date: Tue, 15 Jul 2025 20:05:47 +0300 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20CodeStyle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ru/yandex/practicum/filmorate/dal/BaseDbStorage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/BaseDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/BaseDbStorage.java index 1f170b7..ba00dc4 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/BaseDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/BaseDbStorage.java @@ -51,7 +51,7 @@ protected long insert(String query, Object... params) { for (int idx = 0; idx < params.length; idx++) { ps.setObject(idx + 1, params[idx]); } - return ps;}, keyHolder); + return ps; }, keyHolder); Long id = keyHolder.getKeyAs(Long.class); From 457c8972978c4521c583d62953be1e40dbc92587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=9A=D0=B0?= =?UTF-8?q?=D0=BD?= Date: Tue, 15 Jul 2025 20:07:36 +0300 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20CodeStyle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ru/yandex/practicum/filmorate/dal/GenreDbStorage.java | 4 ---- .../java/ru/yandex/practicum/filmorate/dal/UserDbStorage.java | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/GenreDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/GenreDbStorage.java index 400770b..e7aac81 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/GenreDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/GenreDbStorage.java @@ -7,12 +7,8 @@ import ru.yandex.practicum.filmorate.model.Genre; import ru.yandex.practicum.filmorate.storage.GenreStorage; -import java.util.ArrayList; import java.util.Collection; -import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; @Slf4j @Repository diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/UserDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/UserDbStorage.java index 364832a..77f7d14 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/UserDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/UserDbStorage.java @@ -95,6 +95,7 @@ public Collection findAll() { .collect(Collectors.toSet()) ); }); + return users; } } \ No newline at end of file From 57686086f037f857d7bcfe0d21460a92ae3ee595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=9A=D0=B0?= =?UTF-8?q?=D0=BD?= Date: Tue, 15 Jul 2025 20:13:07 +0300 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20CodeStyle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ru/yandex/practicum/filmorate/dal/UserDbStorage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/UserDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/UserDbStorage.java index 77f7d14..16e3e78 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/UserDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/UserDbStorage.java @@ -83,7 +83,7 @@ public Optional findById(Long id) { public Collection findAll() { Collection users = findMany(FIND_ALL_USERS_QUERY); - if(users.isEmpty()) { + if (users.isEmpty()) { return users; }