diff --git a/README.md b/README.md index af6f295..4a4fdd4 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ - Односторонняя дружба: добавление/удаление друзей, список друзей, общие друзья. - Лайки фильмам: добавление/удаление, топ-N фильмов по количеству лайков. - Отзывы к фильмам: положительные и негативные, с рейтингом полезности по лайкам и дизлайкам. +- Лента событий пользователя: лайки, друзья и отзывы. - Слоистая архитектура: контроллер → сервис → хранилище, выбор реализации хранилища через `@Qualifier`. - Внедрение зависимостей через конструктор (`@Autowired`). - Строгая валидация входящих данных через `spring-boot-starter-validation`. @@ -91,6 +92,8 @@ - `DELETE /users/{id}/friends/{friendId}` — удалить из друзей. - `GET /users/{id}/friends` — список друзей пользователя. - `GET /users/{id}/friends/common/{otherId}` — общие друзья с другим пользователем. +- `GET /users/{id}/feed` — лента событий пользователя по порядку: лайки (`LIKE`), друзья (`FRIEND`) + и отзывы (`REVIEW`) с операциями `ADD`, `REMOVE`, `UPDATE`. ### Отзывы (`/reviews`) - `POST /reviews` — добавить отзыв. @@ -136,6 +139,7 @@ erDiagram films ||--o{ reviews : "film_id" reviews ||--o{ review_likes : "review_id" users ||--o{ review_likes : "user_id" + users ||--o{ feed_events : "user_id" films { BIGINT id PK @@ -192,6 +196,14 @@ erDiagram BIGINT user_id PK, FK BOOLEAN is_useful "TRUE — лайк, FALSE — дизлайк" } + feed_events { + BIGINT event_id PK + BIGINT user_id FK + VARCHAR event_type "LIKE, REVIEW, FRIEND" + VARCHAR operation "ADD, REMOVE, UPDATE" + BIGINT entity_id "id фильма, друга или отзыва" + BIGINT created_at "время события, мс" + } ``` | Таблица | Что хранит | @@ -207,6 +219,7 @@ erDiagram | `film_director` | связь "фильм — режиссёр", многие ко многим | | `reviews` | отзывы пользователей к фильмам; рейтинг полезности не хранится, а считается по `review_likes` | | `review_likes` | оценки отзывов: одна строка — один пользователь, лайк или дизлайк | +| `feed_events` | лента: что пользователь лайкнул, кого добавил в друзья, какие отзывы написал | У таблиц-связок составной первичный ключ, поэтому одна и та же пара не запишется дважды. При удалении фильма или пользователя связанные с ним записи удаляются каскадно. 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 8c154dc..4dbfac8 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -4,10 +4,12 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.model.Event; import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.service.UserService; import java.util.Collection; +import java.util.List; @RestController @RequestMapping("/users") @@ -76,4 +78,10 @@ public Collection getCommonFriends( log.info("GET /users/{}/friends/common/{}", id, otherId); return userService.getCommonFriends(id, otherId); } + + @GetMapping("/{id}/feed") + public List getFeed(@PathVariable long id) { + log.info("GET /users/{}/feed", id); + return userService.getFeed(id); + } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Event.java b/src/main/java/ru/yandex/practicum/filmorate/model/Event.java new file mode 100644 index 0000000..86331a3 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Event.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.Data; + +@Data +public class Event { + private Long eventId; + private Long timestamp; + private Long userId; + private EventType eventType; + private Operation operation; + private Long entityId; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/EventType.java b/src/main/java/ru/yandex/practicum/filmorate/model/EventType.java new file mode 100644 index 0000000..5de71e3 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/EventType.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.model; + +public enum EventType { + LIKE, + REVIEW, + FRIEND +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Operation.java b/src/main/java/ru/yandex/practicum/filmorate/model/Operation.java new file mode 100644 index 0000000..797ccbd --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Operation.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.model; + +public enum Operation { + ADD, + REMOVE, + UPDATE +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FeedService.java b/src/main/java/ru/yandex/practicum/filmorate/service/FeedService.java new file mode 100644 index 0000000..392d84a --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FeedService.java @@ -0,0 +1,36 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.model.Event; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; +import ru.yandex.practicum.filmorate.storage.feed.FeedStorage; + +import java.time.Instant; +import java.util.List; + +@Service +public class FeedService { + + private final FeedStorage feedStorage; + + @Autowired + public FeedService(FeedStorage feedStorage) { + this.feedStorage = feedStorage; + } + + public void addEvent(long userId, EventType eventType, Operation operation, long entityId) { + Event event = new Event(); + event.setTimestamp(Instant.now().toEpochMilli()); + event.setUserId(userId); + event.setEventType(eventType); + event.setOperation(operation); + event.setEntityId(entityId); + feedStorage.add(event); + } + + public List findByUserId(long userId) { + return feedStorage.findByUserId(userId); + } +} 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 3628c9b..8de1336 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -7,8 +7,10 @@ import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.Director; +import ru.yandex.practicum.filmorate.model.EventType; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Operation; import ru.yandex.practicum.filmorate.storage.film.FilmStorage; import ru.yandex.practicum.filmorate.storage.user.UserStorage; @@ -32,18 +34,21 @@ public class FilmService { private final MpaService mpaService; private final GenreService genreService; private final DirectorService directorService; + private final FeedService feedService; @Autowired public FilmService(@Qualifier("filmDbStorage") FilmStorage filmStorage, @Qualifier("userDbStorage") UserStorage userStorage, MpaService mpaService, GenreService genreService, - DirectorService directorService) { + DirectorService directorService, + FeedService feedService) { this.filmStorage = filmStorage; this.userStorage = userStorage; this.mpaService = mpaService; this.genreService = genreService; this.directorService = directorService; + this.feedService = feedService; } // Получение фильма по id с проверкой существования @@ -63,6 +68,7 @@ public Film addLike(long filmId, long userId) { getUserOrThrow(userId); // проверка существования пользователя — иначе 404 filmStorage.addLike(filmId, userId); + feedService.addEvent(userId, EventType.LIKE, Operation.ADD, filmId); log.debug("Пользователь id={} поставил лайк фильму id={}", userId, filmId); return film; } @@ -77,6 +83,7 @@ public Film removeLike(long filmId, long userId) { ); } + feedService.addEvent(userId, EventType.LIKE, Operation.REMOVE, filmId); log.debug("Пользователь id={} убрал лайк с фильма id={}", userId, filmId); return film; } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java b/src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java index bed84b1..10364e8 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java @@ -6,6 +6,8 @@ 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.EventType; +import ru.yandex.practicum.filmorate.model.Operation; import ru.yandex.practicum.filmorate.model.Review; import ru.yandex.practicum.filmorate.storage.film.FilmStorage; import ru.yandex.practicum.filmorate.storage.review.ReviewStorage; @@ -20,14 +22,17 @@ public class ReviewService { private final ReviewStorage reviewStorage; private final FilmStorage filmStorage; private final UserStorage userStorage; + private final FeedService feedService; @Autowired public ReviewService(ReviewStorage reviewStorage, @Qualifier("filmDbStorage") FilmStorage filmStorage, - @Qualifier("userDbStorage") UserStorage userStorage) { + @Qualifier("userDbStorage") UserStorage userStorage, + FeedService feedService) { this.reviewStorage = reviewStorage; this.filmStorage = filmStorage; this.userStorage = userStorage; + this.feedService = feedService; } private Review getReviewOrThrow(long id) { @@ -49,6 +54,7 @@ public Review add(Review review) { checkUserExists(review.getUserId()); checkFilmExists(review.getFilmId()); Review created = reviewStorage.add(review); + feedService.addEvent(created.getUserId(), EventType.REVIEW, Operation.ADD, created.getReviewId()); log.debug("Пользователь id={} оставил отзыв id={} к фильму id={}", created.getUserId(), created.getReviewId(), created.getFilmId()); return created; @@ -60,12 +66,15 @@ public Review update(Review review) { throw new ValidationException("Не указан id отзыва"); } getReviewOrThrow(review.getReviewId()); - return reviewStorage.update(review); + Review updated = reviewStorage.update(review); + feedService.addEvent(updated.getUserId(), EventType.REVIEW, Operation.UPDATE, updated.getReviewId()); + return updated; } public void delete(long id) { - getReviewOrThrow(id); + Review review = getReviewOrThrow(id); reviewStorage.delete(id); + feedService.addEvent(review.getUserId(), EventType.REVIEW, Operation.REMOVE, id); log.debug("Отзыв id={} удалён", id); } 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 cde4926..baa3a43 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -6,20 +6,26 @@ 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.Event; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.storage.user.UserStorage; import java.util.Collection; +import java.util.List; @Service @Slf4j public class UserService { private final UserStorage userStorage; + private final FeedService feedService; @Autowired - public UserService(@Qualifier("userDbStorage") UserStorage userStorage) { + public UserService(@Qualifier("userDbStorage") UserStorage userStorage, FeedService feedService) { this.userStorage = userStorage; + this.feedService = feedService; } private void validateDifferentUsers(long userId, long otherId) { @@ -44,6 +50,7 @@ public User addFriend(long userId, long friendId) { getUserOrThrow(friendId); // проверка существования друга — иначе 404 userStorage.addFriend(userId, friendId); + feedService.addEvent(userId, EventType.FRIEND, Operation.ADD, friendId); log.debug("Пользователь id={} добавил в друзья id={}", userId, friendId); return user; } @@ -55,6 +62,7 @@ public User removeFriend(long userId, long friendId) { getUserOrThrow(friendId); // проверка существования друга — иначе 404 userStorage.removeFriend(userId, friendId); + feedService.addEvent(userId, EventType.FRIEND, Operation.REMOVE, friendId); log.debug("Пользователь id={} удалил из друзей id={}", userId, friendId); return user; } @@ -72,6 +80,11 @@ public Collection getCommonFriends(long userId, long otherId) { return userStorage.findCommonFriends(userId, otherId); } + public List getFeed(long userId) { + getUserOrThrow(userId); + return feedService.findByUserId(userId); + } + // Логин не может содержать пробелы private void validateLogin(User user) { if (user.getLogin().contains(" ")) { diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/feed/FeedDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/feed/FeedDbStorage.java new file mode 100644 index 0000000..a87b0e0 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/feed/FeedDbStorage.java @@ -0,0 +1,53 @@ +package ru.yandex.practicum.filmorate.storage.feed; + +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.Event; +import ru.yandex.practicum.filmorate.storage.mapper.EventRowMapper; + +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.util.List; + +@Repository +public class FeedDbStorage implements FeedStorage { + + private static final String INSERT_QUERY = + "INSERT INTO feed_events (user_id, event_type, operation, entity_id, created_at) VALUES (?, ?, ?, ?, ?)"; + private static final String FIND_BY_USER_QUERY = + "SELECT event_id, user_id, event_type, operation, entity_id, created_at FROM feed_events " + + "WHERE user_id = ? ORDER BY event_id"; + + private final JdbcTemplate jdbc; + private final EventRowMapper mapper; + + @Autowired + public FeedDbStorage(JdbcTemplate jdbc, EventRowMapper mapper) { + this.jdbc = jdbc; + this.mapper = mapper; + } + + @Override + public Event add(Event event) { + KeyHolder keyHolder = new GeneratedKeyHolder(); + jdbc.update(connection -> { + PreparedStatement ps = connection.prepareStatement(INSERT_QUERY, Statement.RETURN_GENERATED_KEYS); + ps.setLong(1, event.getUserId()); + ps.setString(2, event.getEventType().name()); + ps.setString(3, event.getOperation().name()); + ps.setLong(4, event.getEntityId()); + ps.setLong(5, event.getTimestamp()); + return ps; + }, keyHolder); + event.setEventId(keyHolder.getKeyAs(Long.class)); + return event; + } + + @Override + public List findByUserId(long userId) { + return jdbc.query(FIND_BY_USER_QUERY, mapper, userId); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/feed/FeedStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/feed/FeedStorage.java new file mode 100644 index 0000000..96471ca --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/feed/FeedStorage.java @@ -0,0 +1,12 @@ +package ru.yandex.practicum.filmorate.storage.feed; + +import ru.yandex.practicum.filmorate.model.Event; + +import java.util.List; + +public interface FeedStorage { + + Event add(Event event); + + List findByUserId(long userId); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/mapper/EventRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/storage/mapper/EventRowMapper.java new file mode 100644 index 0000000..2725f64 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/mapper/EventRowMapper.java @@ -0,0 +1,26 @@ +package ru.yandex.practicum.filmorate.storage.mapper; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Event; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class EventRowMapper implements RowMapper { + + @Override + public Event mapRow(ResultSet rs, int rowNum) throws SQLException { + Event event = new Event(); + event.setEventId(rs.getLong("event_id")); + event.setTimestamp(rs.getLong("created_at")); + event.setUserId(rs.getLong("user_id")); + event.setEventType(EventType.valueOf(rs.getString("event_type"))); + event.setOperation(Operation.valueOf(rs.getString("operation"))); + event.setEntityId(rs.getLong("entity_id")); + return event; + } +} diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 554a36c..6869731 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -81,4 +81,14 @@ CREATE TABLE IF NOT EXISTS film_director ( film_id BIGINT NOT NULL REFERENCES films(id) ON DELETE CASCADE, director_id BIGINT NOT NULL REFERENCES directors(id) ON DELETE CASCADE, PRIMARY KEY (film_id, director_id) -); \ No newline at end of file +); + +-- Лента: действия пользователя с лайками, друзьями и отзывами. entity_id — id фильма, друга или отзыва. +CREATE TABLE IF NOT EXISTS feed_events ( + event_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + event_type VARCHAR(10) NOT NULL, + operation VARCHAR(10) NOT NULL, + entity_id BIGINT NOT NULL, + created_at BIGINT NOT NULL +); diff --git a/src/test/java/ru/yandex/practicum/filmorate/service/FeedEventsTest.java b/src/test/java/ru/yandex/practicum/filmorate/service/FeedEventsTest.java new file mode 100644 index 0000000..3dff377 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/service/FeedEventsTest.java @@ -0,0 +1,110 @@ +package ru.yandex.practicum.filmorate.service; + +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.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Event; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Operation; +import ru.yandex.practicum.filmorate.model.Review; +import ru.yandex.practicum.filmorate.model.User; + +import java.time.LocalDate; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@AutoConfigureTestDatabase +@Transactional +class FeedEventsTest { + + private final UserService userService; + private final FilmService filmService; + private final ReviewService reviewService; + + @Autowired + FeedEventsTest(UserService userService, FilmService filmService, ReviewService reviewService) { + this.userService = userService; + this.filmService = filmService; + this.reviewService = reviewService; + } + + private User createUser(String login) { + User user = new User(); + user.setEmail(login + "@mail.ru"); + user.setLogin(login); + user.setBirthday(LocalDate.of(1990, 1, 1)); + return userService.add(user); + } + + private Film createFilm() { + Film film = new Film(); + film.setName("Фильм"); + film.setReleaseDate(LocalDate.of(2000, 1, 1)); + film.setDuration(120); + return filmService.add(film); + } + + @Test + void shouldRecordUserActionsInFeed() { + long userId = createUser("anna").getId(); + long friendId = createUser("boris").getId(); + long filmId = createFilm().getId(); + + userService.addFriend(userId, friendId); + userService.removeFriend(userId, friendId); + Review review = new Review(); + review.setContent("Отличный фильм"); + review.setIsPositive(true); + review.setUserId(userId); + review.setFilmId(filmId); + long reviewId = reviewService.add(review).getReviewId(); + review.setReviewId(reviewId); + review.setContent("Так себе фильм"); + reviewService.update(review); + reviewService.delete(reviewId); + filmService.addLike(filmId, userId); + filmService.removeLike(filmId, userId); + + List feed = userService.getFeed(userId); + + assertEquals(List.of( + List.of(EventType.FRIEND, Operation.ADD, friendId), + List.of(EventType.FRIEND, Operation.REMOVE, friendId), + List.of(EventType.REVIEW, Operation.ADD, reviewId), + List.of(EventType.REVIEW, Operation.UPDATE, reviewId), + List.of(EventType.REVIEW, Operation.REMOVE, reviewId), + List.of(EventType.LIKE, Operation.ADD, filmId), + List.of(EventType.LIKE, Operation.REMOVE, filmId) + ), feed.stream().map(e -> List.of(e.getEventType(), e.getOperation(), e.getEntityId())).toList()); + assertTrue(feed.stream().allMatch(e -> e.getUserId() == userId && e.getTimestamp() > 0)); + assertTrue(userService.getFeed(friendId).isEmpty()); + } + + @Test + void shouldNotRecordReviewRatings() { + long authorId = createUser("anna").getId(); + long raterId = createUser("boris").getId(); + Review review = new Review(); + review.setContent("Отличный фильм"); + review.setIsPositive(true); + review.setUserId(authorId); + review.setFilmId(createFilm().getId()); + long reviewId = reviewService.add(review).getReviewId(); + + reviewService.addLike(reviewId, raterId); + reviewService.addDislike(reviewId, raterId); + + assertTrue(userService.getFeed(raterId).isEmpty()); + } + + @Test + void shouldThrowWhenFeedOfUnknownUser() { + assertThrows(NotFoundException.class, () -> userService.getFeed(9999)); + } +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/feed/FeedDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/feed/FeedDbStorageTest.java new file mode 100644 index 0000000..6da9dab --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/feed/FeedDbStorageTest.java @@ -0,0 +1,80 @@ +package ru.yandex.practicum.filmorate.storage.feed; + +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.Event; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.mapper.EventRowMapper; +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({FeedDbStorage.class, EventRowMapper.class, UserDbStorage.class, UserRowMapper.class}) +class FeedDbStorageTest { + + private final FeedDbStorage feedStorage; + private final UserDbStorage userStorage; + + @Autowired + FeedDbStorageTest(FeedDbStorage feedStorage, UserDbStorage userStorage) { + this.feedStorage = feedStorage; + this.userStorage = userStorage; + } + + 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 Event addEvent(long userId, EventType eventType, Operation operation, long entityId) { + Event event = new Event(); + event.setTimestamp(1_700_000_000_000L); + event.setUserId(userId); + event.setEventType(eventType); + event.setOperation(operation); + event.setEntityId(entityId); + return feedStorage.add(event); + } + + @Test + void shouldReturnUserEventsInOrderOfAdding() { + User anna = createUser("anna"); + User boris = createUser("boris"); + Event first = addEvent(anna.getId(), EventType.FRIEND, Operation.ADD, boris.getId()); + addEvent(boris.getId(), EventType.LIKE, Operation.ADD, 1); + Event second = addEvent(anna.getId(), EventType.REVIEW, Operation.UPDATE, 5); + + assertNotNull(first.getEventId()); + assertEquals(List.of(first, second), feedStorage.findByUserId(anna.getId())); + } + + @Test + void shouldReturnEmptyFeedForUserWithoutEvents() { + assertTrue(feedStorage.findByUserId(createUser("anna").getId()).isEmpty()); + } + + @Test + void shouldDeleteEventsWithUser() { + User anna = createUser("anna"); + addEvent(anna.getId(), EventType.LIKE, Operation.ADD, 1); + + userStorage.delete(anna.getId()); + + assertTrue(feedStorage.findByUserId(anna.getId()).isEmpty()); + } +}