diff --git a/README.md b/README.md index 50b4159..fbad3e1 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ - У фильма есть рейтинг и список жанров; жанры в ответе — без дублей, по порядку id. - Односторонняя дружба: добавление/удаление друзей, список друзей, общие друзья. - Лайки фильмам: добавление/удаление, топ-N фильмов по количеству лайков. +- Отзывы к фильмам: положительные и негативные, с рейтингом полезности по лайкам и дизлайкам. - Слоистая архитектура: контроллер → сервис → хранилище, выбор реализации хранилища через `@Qualifier`. - Внедрение зависимостей через конструктор (`@Autowired`). - Строгая валидация входящих данных через `spring-boot-starter-validation`. @@ -81,6 +82,18 @@ - `GET /users/{id}/friends` — список друзей пользователя. - `GET /users/{id}/friends/common/{otherId}` — общие друзья с другим пользователем. +### Отзывы (`/reviews`) +- `POST /reviews` — добавить отзыв. +- `PUT /reviews` — изменить текст и тип отзыва. +- `DELETE /reviews/{id}` — удалить отзыв. +- `GET /reviews/{id}` — получить отзыв по ID. +- `GET /reviews?filmId={filmId}&count={count}` — отзывы к фильму (без `filmId` — ко всем), + по убыванию полезности; по умолчанию 10. +- `PUT /reviews/{id}/like/{userId}` — отметить отзыв полезным. +- `PUT /reviews/{id}/dislike/{userId}` — отметить отзыв бесполезным. +- `DELETE /reviews/{id}/like/{userId}` — убрать лайк. +- `DELETE /reviews/{id}/dislike/{userId}` — убрать дизлайк. + ### Жанры (`/genres`) - `GET /genres` — получить список всех жанров. - `GET /genres/{id}` — получить жанр по ID. @@ -100,6 +113,10 @@ erDiagram users ||--o{ film_likes : "user_id" users ||--o{ friendship : "user_id" users ||--o{ friendship : "friend_id" + users ||--o{ reviews : "user_id" + films ||--o{ reviews : "film_id" + reviews ||--o{ review_likes : "review_id" + users ||--o{ review_likes : "user_id" films { BIGINT id PK @@ -136,6 +153,18 @@ erDiagram BIGINT user_id PK, FK BIGINT friend_id PK, FK } + reviews { + BIGINT id PK + VARCHAR content + BOOLEAN is_positive + BIGINT user_id FK + BIGINT film_id FK + } + review_likes { + BIGINT review_id PK, FK + BIGINT user_id PK, FK + BOOLEAN is_useful "TRUE — лайк, FALSE — дизлайк" + } ``` | Таблица | Что хранит | @@ -147,6 +176,8 @@ erDiagram | `film_genres` | связь «фильм — жанр», многие ко многим | | `film_likes` | лайки: какой пользователь лайкнул какой фильм | | `friendship` | дружба: одна строка — `user_id` добавил в друзья `friend_id`; подтверждённая (взаимная) дружба — две встречные строки | +| `reviews` | отзывы пользователей к фильмам; рейтинг полезности не хранится, а считается по `review_likes` | +| `review_likes` | оценки отзывов: одна строка — один пользователь, лайк или дизлайк | У таблиц-связок составной первичный ключ, поэтому одна и та же пара не запишется дважды. При удалении фильма или пользователя связанные с ним записи удаляются каскадно. diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java new file mode 100644 index 0000000..e84423a --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java @@ -0,0 +1,79 @@ +package ru.yandex.practicum.filmorate.controller; + +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.model.Review; +import ru.yandex.practicum.filmorate.service.ReviewService; + +import java.util.Collection; + +@RestController +@RequestMapping("/reviews") +@Slf4j +public class ReviewController { + + private final ReviewService reviewService; + + @Autowired + public ReviewController(ReviewService reviewService) { + this.reviewService = reviewService; + } + + @PostMapping + public Review create(@Valid @RequestBody Review review) { + log.info("POST /reviews : userId={}, filmId={}", review.getUserId(), review.getFilmId()); + return reviewService.add(review); + } + + @PutMapping + public Review update(@Valid @RequestBody Review review) { + log.info("PUT /reviews : id={}", review.getReviewId()); + return reviewService.update(review); + } + + @DeleteMapping("/{id}") + public void delete(@PathVariable long id) { + log.info("DELETE /reviews/{}", id); + reviewService.delete(id); + } + + @GetMapping("/{id}") + public Review findById(@PathVariable long id) { + log.info("GET /reviews/{}", id); + return reviewService.findById(id); + } + + @GetMapping + public Collection findMostUseful( + @RequestParam(required = false) Long filmId, + @RequestParam(defaultValue = "10") int count) { + log.info("GET /reviews?filmId={}&count={}", filmId, count); + return reviewService.findMostUseful(filmId, count); + } + + @PutMapping("/{id}/like/{userId}") + public void addLike(@PathVariable long id, @PathVariable long userId) { + log.info("PUT /reviews/{}/like/{}", id, userId); + reviewService.addLike(id, userId); + } + + @PutMapping("/{id}/dislike/{userId}") + public void addDislike(@PathVariable long id, @PathVariable long userId) { + log.info("PUT /reviews/{}/dislike/{}", id, userId); + reviewService.addDislike(id, userId); + } + + @DeleteMapping("/{id}/like/{userId}") + public void removeLike(@PathVariable long id, @PathVariable long userId) { + log.info("DELETE /reviews/{}/like/{}", id, userId); + reviewService.removeLike(id, userId); + } + + @DeleteMapping("/{id}/dislike/{userId}") + public void removeDislike(@PathVariable long id, @PathVariable long userId) { + log.info("DELETE /reviews/{}/dislike/{}", id, userId); + reviewService.removeDislike(id, userId); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Review.java b/src/main/java/ru/yandex/practicum/filmorate/model/Review.java new file mode 100644 index 0000000..e8a4c41 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Review.java @@ -0,0 +1,24 @@ +package ru.yandex.practicum.filmorate.model; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class Review { + private Long reviewId; + + @NotBlank(message = "Текст отзыва не может быть пустым") + private String content; + + @NotNull(message = "Тип отзыва должен быть указан") + private Boolean isPositive; + + @NotNull(message = "Автор отзыва должен быть указан") + private Long userId; + + @NotNull(message = "Фильм должен быть указан") + private Long filmId; + + private int useful; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java b/src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java new file mode 100644 index 0000000..bed84b1 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java @@ -0,0 +1,118 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ValidationException; +import ru.yandex.practicum.filmorate.model.Review; +import ru.yandex.practicum.filmorate.storage.film.FilmStorage; +import ru.yandex.practicum.filmorate.storage.review.ReviewStorage; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; + +import java.util.Collection; + +@Service +@Slf4j +public class ReviewService { + + private final ReviewStorage reviewStorage; + private final FilmStorage filmStorage; + private final UserStorage userStorage; + + @Autowired + public ReviewService(ReviewStorage reviewStorage, + @Qualifier("filmDbStorage") FilmStorage filmStorage, + @Qualifier("userDbStorage") UserStorage userStorage) { + this.reviewStorage = reviewStorage; + this.filmStorage = filmStorage; + this.userStorage = userStorage; + } + + private Review getReviewOrThrow(long id) { + return reviewStorage.findById(id) + .orElseThrow(() -> new NotFoundException("Отзыв с id=" + id + " не найден")); + } + + private void checkUserExists(long id) { + userStorage.findById(id) + .orElseThrow(() -> new NotFoundException("Пользователь с id=" + id + " не найден")); + } + + private void checkFilmExists(long id) { + filmStorage.findById(id) + .orElseThrow(() -> new NotFoundException("Фильм с id=" + id + " не найден")); + } + + public Review add(Review review) { + checkUserExists(review.getUserId()); + checkFilmExists(review.getFilmId()); + Review created = reviewStorage.add(review); + log.debug("Пользователь id={} оставил отзыв id={} к фильму id={}", + created.getUserId(), created.getReviewId(), created.getFilmId()); + return created; + } + + // Автор, фильм и рейтинг полезности не меняются — только текст и тип отзыва + public Review update(Review review) { + if (review.getReviewId() == null) { + throw new ValidationException("Не указан id отзыва"); + } + getReviewOrThrow(review.getReviewId()); + return reviewStorage.update(review); + } + + public void delete(long id) { + getReviewOrThrow(id); + reviewStorage.delete(id); + log.debug("Отзыв id={} удалён", id); + } + + public Review findById(long id) { + return getReviewOrThrow(id); + } + + public Collection findMostUseful(Long filmId, int count) { + if (count <= 0) { + throw new ValidationException( + "Количество отзывов должно быть положительным числом, получено: " + count + ); + } + if (filmId != null) { + checkFilmExists(filmId); + } + return reviewStorage.findMostUseful(filmId, count); + } + + public void addLike(long reviewId, long userId) { + addRating(reviewId, userId, true); + } + + public void addDislike(long reviewId, long userId) { + addRating(reviewId, userId, false); + } + + public void removeLike(long reviewId, long userId) { + removeRating(reviewId, userId, true); + } + + public void removeDislike(long reviewId, long userId) { + removeRating(reviewId, userId, false); + } + + // Повторная оценка заменяет предыдущую: лайк после дизлайка меняет рейтинг на +2 + private void addRating(long reviewId, long userId, boolean isUseful) { + getReviewOrThrow(reviewId); + checkUserExists(userId); + reviewStorage.addRating(reviewId, userId, isUseful); + log.debug("Пользователь id={} оценил отзыв id={}: полезный={}", userId, reviewId, isUseful); + } + + private void removeRating(long reviewId, long userId, boolean isUseful) { + getReviewOrThrow(reviewId); + checkUserExists(userId); + reviewStorage.removeRating(reviewId, userId, isUseful); + log.debug("Пользователь id={} убрал оценку отзыва id={}: полезный={}", userId, reviewId, isUseful); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/mapper/ReviewRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/storage/mapper/ReviewRowMapper.java new file mode 100644 index 0000000..0132197 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/mapper/ReviewRowMapper.java @@ -0,0 +1,24 @@ +package ru.yandex.practicum.filmorate.storage.mapper; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Review; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class ReviewRowMapper implements RowMapper { + + @Override + public Review mapRow(ResultSet rs, int rowNum) throws SQLException { + Review review = new Review(); + review.setReviewId(rs.getLong("id")); + review.setContent(rs.getString("content")); + review.setIsPositive(rs.getBoolean("is_positive")); + review.setUserId(rs.getLong("user_id")); + review.setFilmId(rs.getLong("film_id")); + review.setUseful(rs.getInt("useful")); + return review; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/review/ReviewDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/review/ReviewDbStorage.java new file mode 100644 index 0000000..a292be8 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/review/ReviewDbStorage.java @@ -0,0 +1,96 @@ +package ru.yandex.practicum.filmorate.storage.review; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.Review; +import ru.yandex.practicum.filmorate.storage.mapper.ReviewRowMapper; + +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.util.List; +import java.util.Optional; + +@Repository +public class ReviewDbStorage implements ReviewStorage { + + // Рейтинг полезности: лайк +1, дизлайк -1 + private static final String SELECT_REVIEWS = + "SELECT r.id, r.content, r.is_positive, r.user_id, r.film_id, " + + "(SELECT COALESCE(SUM(CASE WHEN rl.is_useful THEN 1 ELSE -1 END), 0) " + + "FROM review_likes rl WHERE rl.review_id = r.id) AS useful " + + "FROM reviews r "; + private static final String FIND_BY_ID_QUERY = SELECT_REVIEWS + "WHERE r.id = ?"; + private static final String FIND_ALL_QUERY = SELECT_REVIEWS + + "ORDER BY useful DESC, r.id LIMIT ?"; + private static final String FIND_BY_FILM_QUERY = SELECT_REVIEWS + + "WHERE r.film_id = ? ORDER BY useful DESC, r.id LIMIT ?"; + private static final String INSERT_QUERY = + "INSERT INTO reviews (content, is_positive, user_id, film_id) VALUES (?, ?, ?, ?)"; + private static final String UPDATE_QUERY = + "UPDATE reviews SET content = ?, is_positive = ? WHERE id = ?"; + private static final String DELETE_QUERY = "DELETE FROM reviews WHERE id = ?"; + private static final String ADD_RATING_QUERY = + "MERGE INTO review_likes (review_id, user_id, is_useful) KEY (review_id, user_id) VALUES (?, ?, ?)"; + private static final String REMOVE_RATING_QUERY = + "DELETE FROM review_likes WHERE review_id = ? AND user_id = ? AND is_useful = ?"; + + private final JdbcTemplate jdbc; + private final ReviewRowMapper mapper; + + @Autowired + public ReviewDbStorage(JdbcTemplate jdbc, ReviewRowMapper mapper) { + this.jdbc = jdbc; + this.mapper = mapper; + } + + @Override + public Review add(Review review) { + KeyHolder keyHolder = new GeneratedKeyHolder(); + jdbc.update(connection -> { + PreparedStatement ps = connection.prepareStatement(INSERT_QUERY, Statement.RETURN_GENERATED_KEYS); + ps.setString(1, review.getContent()); + ps.setBoolean(2, review.getIsPositive()); + ps.setLong(3, review.getUserId()); + ps.setLong(4, review.getFilmId()); + return ps; + }, keyHolder); + return findById(keyHolder.getKeyAs(Long.class)).orElseThrow(); + } + + @Override + public Review update(Review review) { + jdbc.update(UPDATE_QUERY, review.getContent(), review.getIsPositive(), review.getReviewId()); + return findById(review.getReviewId()).orElseThrow(); + } + + @Override + public void delete(long id) { + jdbc.update(DELETE_QUERY, id); + } + + @Override + public Optional findById(long id) { + return jdbc.query(FIND_BY_ID_QUERY, mapper, id).stream().findFirst(); + } + + @Override + public List findMostUseful(Long filmId, int count) { + if (filmId == null) { + return jdbc.query(FIND_ALL_QUERY, mapper, count); + } + return jdbc.query(FIND_BY_FILM_QUERY, mapper, filmId, count); + } + + @Override + public void addRating(long reviewId, long userId, boolean isUseful) { + jdbc.update(ADD_RATING_QUERY, reviewId, userId, isUseful); + } + + @Override + public void removeRating(long reviewId, long userId, boolean isUseful) { + jdbc.update(REMOVE_RATING_QUERY, reviewId, userId, isUseful); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/review/ReviewStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/review/ReviewStorage.java new file mode 100644 index 0000000..5eaa3b4 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/review/ReviewStorage.java @@ -0,0 +1,23 @@ +package ru.yandex.practicum.filmorate.storage.review; + +import ru.yandex.practicum.filmorate.model.Review; + +import java.util.List; +import java.util.Optional; + +public interface ReviewStorage { + + Review add(Review review); + + Review update(Review review); + + void delete(long id); + + Optional findById(long id); + + List findMostUseful(Long filmId, int count); + + void addRating(long reviewId, long userId, boolean isUseful); + + void removeRating(long reviewId, long userId, boolean isUseful); +} diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 3d660c3..b2dcd2d 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -52,4 +52,20 @@ CREATE TABLE IF NOT EXISTS friendship ( user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, friend_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, PRIMARY KEY (user_id, friend_id) -); \ No newline at end of file +); + +CREATE TABLE IF NOT EXISTS reviews ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + content VARCHAR NOT NULL, + is_positive BOOLEAN NOT NULL, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + film_id BIGINT NOT NULL REFERENCES films(id) ON DELETE CASCADE +); + +-- Оценка отзыва: is_useful = TRUE — лайк, FALSE — дизлайк. Один пользователь — одна оценка. +CREATE TABLE IF NOT EXISTS review_likes ( + review_id BIGINT NOT NULL REFERENCES reviews(id) ON DELETE CASCADE, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + is_useful BOOLEAN NOT NULL, + PRIMARY KEY (review_id, user_id) +); diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/review/ReviewDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/review/ReviewDbStorageTest.java new file mode 100644 index 0000000..95d380c --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/review/ReviewDbStorageTest.java @@ -0,0 +1,185 @@ +package ru.yandex.practicum.filmorate.storage.review; + +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 ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Review; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.film.FilmDbStorage; +import ru.yandex.practicum.filmorate.storage.mapper.FilmRowMapper; +import ru.yandex.practicum.filmorate.storage.mapper.GenreRowMapper; +import ru.yandex.practicum.filmorate.storage.mapper.ReviewRowMapper; +import ru.yandex.practicum.filmorate.storage.mapper.UserRowMapper; +import ru.yandex.practicum.filmorate.storage.user.UserDbStorage; + +import java.time.LocalDate; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@JdbcTest +@AutoConfigureTestDatabase +@Import({ReviewDbStorage.class, ReviewRowMapper.class, FilmDbStorage.class, FilmRowMapper.class, + GenreRowMapper.class, UserDbStorage.class, UserRowMapper.class}) +class ReviewDbStorageTest { + + private final ReviewDbStorage reviewStorage; + private final FilmDbStorage filmStorage; + private final UserDbStorage userStorage; + + private User author; + private Film film; + + @Autowired + ReviewDbStorageTest(ReviewDbStorage reviewStorage, FilmDbStorage filmStorage, UserDbStorage userStorage) { + this.reviewStorage = reviewStorage; + this.filmStorage = filmStorage; + this.userStorage = userStorage; + } + + @BeforeEach + void setUp() { + author = createUser("author"); + film = createFilm("Фильм"); + } + + private User createUser(String login) { + User user = new User(); + user.setEmail(login + "@mail.ru"); + user.setLogin(login); + user.setName("Пользователь " + login); + user.setBirthday(LocalDate.of(1990, 1, 1)); + return userStorage.add(user); + } + + private Film createFilm(String name) { + Film newFilm = new Film(); + newFilm.setName(name); + newFilm.setDescription("Описание"); + newFilm.setReleaseDate(LocalDate.of(2000, 1, 1)); + newFilm.setDuration(120); + return filmStorage.add(newFilm); + } + + private Review createReview(long filmId, String content) { + Review review = new Review(); + review.setContent(content); + review.setIsPositive(true); + review.setUserId(author.getId()); + review.setFilmId(filmId); + return reviewStorage.add(review); + } + + @Test + void shouldAddReviewAndFindById() { + Review review = createReview(film.getId(), "Отличный фильм"); + + assertNotNull(review.getReviewId()); + assertEquals(0, review.getUseful()); + assertEquals(review, reviewStorage.findById(review.getReviewId()).orElseThrow()); + } + + @Test + void shouldReturnEmptyWhenReviewNotFound() { + assertTrue(reviewStorage.findById(9999).isEmpty()); + } + + @Test + void shouldUpdateOnlyContentAndType() { + Review review = createReview(film.getId(), "Отличный фильм"); + User other = createUser("other"); + Review changes = new Review(); + changes.setReviewId(review.getReviewId()); + changes.setContent("Так себе фильм"); + changes.setIsPositive(false); + changes.setUserId(other.getId()); + changes.setFilmId(createFilm("Другой").getId()); + + Review updated = reviewStorage.update(changes); + + assertEquals("Так себе фильм", updated.getContent()); + assertFalse(updated.getIsPositive()); + assertEquals(author.getId(), updated.getUserId()); + assertEquals(film.getId(), updated.getFilmId()); + } + + @Test + void shouldDeleteReview() { + Review review = createReview(film.getId(), "Отличный фильм"); + + reviewStorage.delete(review.getReviewId()); + + assertTrue(reviewStorage.findById(review.getReviewId()).isEmpty()); + } + + @Test + void shouldCountUsefulFromLikesAndDislikes() { + Review review = createReview(film.getId(), "Отличный фильм"); + long reviewId = review.getReviewId(); + reviewStorage.addRating(reviewId, createUser("first").getId(), true); + reviewStorage.addRating(reviewId, createUser("second").getId(), true); + reviewStorage.addRating(reviewId, createUser("third").getId(), false); + + assertEquals(1, reviewStorage.findById(reviewId).orElseThrow().getUseful()); + } + + @Test + void shouldReplaceLikeWithDislike() { + Review review = createReview(film.getId(), "Отличный фильм"); + long reviewId = review.getReviewId(); + long userId = createUser("rater").getId(); + reviewStorage.addRating(reviewId, userId, true); + + reviewStorage.addRating(reviewId, userId, false); + + assertEquals(-1, reviewStorage.findById(reviewId).orElseThrow().getUseful()); + } + + @Test + void shouldRemoveOnlyMatchingRating() { + Review review = createReview(film.getId(), "Отличный фильм"); + long reviewId = review.getReviewId(); + long userId = createUser("rater").getId(); + reviewStorage.addRating(reviewId, userId, true); + + reviewStorage.removeRating(reviewId, userId, false); + assertEquals(1, reviewStorage.findById(reviewId).orElseThrow().getUseful()); + + reviewStorage.removeRating(reviewId, userId, true); + assertEquals(0, reviewStorage.findById(reviewId).orElseThrow().getUseful()); + } + + @Test + void shouldSortByUsefulAndFilterByFilm() { + Review first = createReview(film.getId(), "Первый"); + Review second = createReview(film.getId(), "Второй"); + Review third = createReview(film.getId(), "Третий"); + Review otherFilmReview = createReview(createFilm("Другой").getId(), "Про другой фильм"); + reviewStorage.addRating(second.getReviewId(), createUser("rater").getId(), true); + reviewStorage.addRating(first.getReviewId(), createUser("hater").getId(), false); + + List byFilm = reviewStorage.findMostUseful(film.getId(), 10).stream() + .map(Review::getReviewId) + .toList(); + assertEquals(List.of(second.getReviewId(), third.getReviewId(), first.getReviewId()), byFilm); + + assertEquals(4, reviewStorage.findMostUseful(null, 10).size()); + assertEquals(List.of(second.getReviewId(), third.getReviewId()), + reviewStorage.findMostUseful(null, 2).stream().map(Review::getReviewId).toList()); + assertTrue(reviewStorage.findMostUseful(film.getId(), 10).stream() + .noneMatch(review -> review.getReviewId().equals(otherFilmReview.getReviewId()))); + } + + @Test + void shouldDeleteReviewsWithFilm() { + Review review = createReview(film.getId(), "Отличный фильм"); + + filmStorage.delete(film.getId()); + + assertTrue(reviewStorage.findById(review.getReviewId()).isEmpty()); + } +}