From 3d442a2a2ba20f1f86b22b40cbd95ffac021b95a Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:04:50 +0300 Subject: [PATCH 01/26] fix: updated pom and config files --- pom.xml | 10 ++++++++++ src/main/resources/application-dev.properties | 17 ++++++++++++++--- src/main/resources/application.properties | 8 +++++++- src/test/resources/application.properties | 11 ++++++++++- 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 0ffcf4b..3c38505 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,16 @@ test + + org.springframework.boot + spring-boot-starter-jdbc + + + + com.h2database + h2 + + diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index b50b2c2..3ac1048 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -1,5 +1,5 @@ spring.application.name=Filmorate -server.port=8081 +server.port=8080 logging.file.path=./logs/ logging.logback.rollingpolicy.file-name-pattern=./logs/spring.%d{yyyy-MM-dd}.%i.log @@ -8,5 +8,16 @@ logging.logback.rollingpolicy.total-size-cap=50MB logging.logback.rollingpolicy.clean-history-on-start=true spring.output.ansi.enabled=ALWAYS -logging.level.ru.yandex.practicum.filmorate=INFO -logging.level.org.zalando.logbook=TRACE \ No newline at end of file +logging.level.ru.yandex.practicum.filmorate=DEBUG +logging.level.org.zalando.logbook=DEBUG +logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG +logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG +logging.level.com.zaxxer.hikari=DEBUG + +spring.sql.init.mode=always +#spring.datasource.url=jdbc:h2:file:./db/filmorate +spring.datasource.url=jdbc:h2:mem:filmorate +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=admin +spring.datasource.password=admin +spring.h2.console.enabled=true \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 45001af..b57abfb 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,4 +9,10 @@ logging.logback.rollingpolicy.clean-history-on-start=true spring.output.ansi.enabled=ALWAYS logging.level.ru.yandex.practicum.filmorate=WARN -logging.level.org.zalando.logbook=WARN \ No newline at end of file +logging.level.org.zalando.logbook=WARN + +spring.sql.init.mode=always +spring.datasource.url=jdbc:h2:file:./db/filmorate +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=admin +spring.datasource.password=admin \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 947989f..6f392d3 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -1,3 +1,12 @@ logging.level.ru.yandex.practicum.filmorate=OFF logging.level.org.springframework=WARN -spring.main.banner-mode=off \ No newline at end of file +spring.main.banner-mode=off + +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +spring.sql.init.mode=always +spring.sql.init.schema-locations=classpath:schema.sql +spring.sql.init.data-locations=classpath:data.sql \ No newline at end of file From 15639a28137a3322a21f20019a645c603724b82c Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:05:08 +0300 Subject: [PATCH 02/26] feat: add sql files for db --- src/main/resources/data.sql | 20 ++++++++++++++++ src/main/resources/schema.sql | 44 +++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 src/main/resources/data.sql create mode 100644 src/main/resources/schema.sql diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 0000000..2e931bd --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,20 @@ +DELETE FROM genres; +DELETE FROM mpa; + +ALTER TABLE genres ALTER COLUMN genre_id RESTART WITH 1; +ALTER TABLE mpa ALTER COLUMN mpa_id RESTART WITH 1; + +INSERT INTO mpa (name) VALUES +('G'), +('PG'), +('PG-13'), +('R'), +('NC-17'); + +INSERT INTO genres (name) VALUES +('Комедия'), +('Драма'), +('Мультфильм'), +('Триллер'), +('Документальный'), +('Боевик'); \ 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..a1597a0 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,44 @@ +CREATE TABLE IF NOT EXISTS users ( + user_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + email VARCHAR NOT NULL, + login VARCHAR NOT NULL, + name VARCHAR, + birthday DATE); + +CREATE TABLE IF NOT EXISTS friendships ( + user_id BIGINT, + friend_id BIGINT, + FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, + FOREIGN KEY (friend_id) REFERENCES users(user_id) ON DELETE CASCADE, + PRIMARY KEY (user_id, friend_id)); + +CREATE TABLE IF NOT EXISTS mpa ( + mpa_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name VARCHAR NOT NULL); + +CREATE TABLE IF NOT EXISTS films ( + film_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name VARCHAR NOT NULL, + description VARCHAR(200), + release_date DATE, + duration INTEGER, + mpa_id BIGINT, + FOREIGN KEY (mpa_id) REFERENCES mpa(mpa_id) ON DELETE SET NULL); + +CREATE TABLE IF NOT EXISTS genres ( + genre_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name VARCHAR NOT NULL); + +CREATE TABLE IF NOT EXISTS films_genres ( + film_id BIGINT, + genre_id BIGINT, + FOREIGN KEY (film_id) REFERENCES films(film_id) ON DELETE CASCADE, + FOREIGN KEY (genre_id) REFERENCES genres(genre_id) ON DELETE CASCADE, + PRIMARY KEY (film_id, genre_id)); + +CREATE TABLE IF NOT EXISTS likes ( + film_id BIGINT, + user_id BIGINT, + FOREIGN KEY (film_id) REFERENCES films(film_id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, + PRIMARY KEY (film_id, user_id)); \ No newline at end of file From 53e6b2d7cb0048c5ac85ef0af6258ac729e8e13c Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:05:49 +0300 Subject: [PATCH 03/26] feat: Genre and Mpa model --- .../yandex/practicum/filmorate/model/Genre.java | 16 ++++++++++++++++ .../ru/yandex/practicum/filmorate/model/Mpa.java | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/Genre.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/Mpa.java 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..b422c45 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java @@ -0,0 +1,16 @@ +package ru.yandex.practicum.filmorate.model; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Genre { + + @NotNull + private Long id; + private String name; +} 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..bc8733e --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Mpa.java @@ -0,0 +1,16 @@ +package ru.yandex.practicum.filmorate.model; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Mpa { + + @NotNull + private Long id; + private String name; +} From 6d9c86edd9fd8c755df157446406735c470684cc Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:05:59 +0300 Subject: [PATCH 04/26] feat: Genre and Mpa service --- .../filmorate/service/film/GenreService.java | 27 +++++++++++++++++++ .../filmorate/service/film/MpaService.java | 27 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/film/GenreService.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/film/MpaService.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/film/GenreService.java b/src/main/java/ru/yandex/practicum/filmorate/service/film/GenreService.java new file mode 100644 index 0000000..dd774a9 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/film/GenreService.java @@ -0,0 +1,27 @@ +package ru.yandex.practicum.filmorate.service.film; + +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.film.GenreStorage; + +import java.util.Collection; + +@Service +public class GenreService { + + private final GenreStorage storage; + + public GenreService(GenreStorage storage) { + this.storage = storage; + } + + public Genre getGenre(Long id) { + return storage.getGenre(id) + .orElseThrow(() -> new NotFoundException("Жанр с id = " + id + " не найден")); + } + + public Collection getAll() { + return storage.getAll(); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/film/MpaService.java b/src/main/java/ru/yandex/practicum/filmorate/service/film/MpaService.java new file mode 100644 index 0000000..38353e7 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/film/MpaService.java @@ -0,0 +1,27 @@ +package ru.yandex.practicum.filmorate.service.film; + +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.film.MpaStorage; + +import java.util.Collection; + +@Service +public class MpaService { + + private final MpaStorage storage; + + public MpaService(MpaStorage storage) { + this.storage = storage; + } + + public Mpa getMpa(Long id) { + return storage.getMpa(id) + .orElseThrow(() -> new NotFoundException("Mpa с id = " + id + " не найден")); + } + + public Collection getAll() { + return storage.getAll(); + } +} From 73e0de66892bfa0cbe5919d3c78b216c0e8dcb18 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:06:07 +0300 Subject: [PATCH 05/26] feat: Genre and Mpa controller --- .../filmorate/controller/GenreController.java | 32 +++++++++++++++++++ .../filmorate/controller/MpaController.java | 32 +++++++++++++++++++ 2 files changed, 64 insertions(+) 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 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..45673bc --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java @@ -0,0 +1,32 @@ +package ru.yandex.practicum.filmorate.controller; + +import jakarta.validation.constraints.Positive; +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.film.GenreService; + +import java.util.Collection; + +@RestController +@RequestMapping("/genres") +public class GenreController { + + GenreService service; + + public GenreController(GenreService service) { + this.service = service; + } + + @GetMapping("/{id}") + public Genre getGenre(@PathVariable @Positive Long id) { + return service.getGenre(id); + } + + @GetMapping + public Collection getAll() { + return service.getAll(); + } +} \ 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..20c8ff9 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java @@ -0,0 +1,32 @@ +package ru.yandex.practicum.filmorate.controller; + +import jakarta.validation.constraints.Positive; +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.film.MpaService; + +import java.util.Collection; + +@RestController +@RequestMapping("/mpa") +public class MpaController { + + MpaService service; + + public MpaController(MpaService service) { + this.service = service; + } + + @GetMapping("/{id}") + public Mpa getMpa(@PathVariable @Positive Long id) { + return service.getMpa(id); + } + + @GetMapping + public Collection getAll() { + return service.getAll(); + } +} From fdf68a54a5945803c17976d28c12328d2ef3c056 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:07:06 +0300 Subject: [PATCH 06/26] feat: add Genre and Mpa db storages --- .../filmorate/storage/film/GenreStorage.java | 18 ++++++ .../filmorate/storage/film/MpaStorage.java | 13 ++++ .../storage/film/db/DbGenreStorage.java | 60 +++++++++++++++++++ .../storage/film/db/DbMpaStorage.java | 38 ++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/film/GenreStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/film/MpaStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/film/db/DbGenreStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/film/db/DbMpaStorage.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/GenreStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/GenreStorage.java new file mode 100644 index 0000000..f30feba --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/GenreStorage.java @@ -0,0 +1,18 @@ +package ru.yandex.practicum.filmorate.storage.film; + +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Genre; + +import java.util.Collection; +import java.util.Optional; + +public interface GenreStorage { + + Optional getGenre(Long id); + + Collection getAll(); + + void addGenresToFilm(Film film); + + void updateFilmGenres(Film film); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/MpaStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/MpaStorage.java new file mode 100644 index 0000000..01558fc --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/MpaStorage.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.storage.film; + +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.util.Collection; +import java.util.Optional; + +public interface MpaStorage { + + Optional getMpa(Long id); + + Collection getAll(); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/db/DbGenreStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/db/DbGenreStorage.java new file mode 100644 index 0000000..095b7d3 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/db/DbGenreStorage.java @@ -0,0 +1,60 @@ +package ru.yandex.practicum.filmorate.storage.film.db; + +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.storage.BaseDao; +import ru.yandex.practicum.filmorate.storage.film.GenreStorage; +import ru.yandex.practicum.filmorate.storage.mappers.GenreMapper; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +@Repository("DbGenreStorage") +public class DbGenreStorage extends BaseDao implements GenreStorage { + + private static final String SELECT_BY_ID_QUERY = "SELECT * FROM genres WHERE genre_id = ?"; + private static final String SELECT_ALL_QUERY = "SELECT * FROM genres;"; + + public DbGenreStorage(JdbcTemplate jdbc, GenreMapper mapper) { + super(jdbc, mapper); + } + + @Override + public Optional getGenre(Long id) { + try { + Genre genre = get(SELECT_BY_ID_QUERY, id); + return Optional.ofNullable(genre); + } catch (EmptyResultDataAccessException e) { + return Optional.empty(); + } + } + + @Override + public Collection getAll() { + return getAll(SELECT_ALL_QUERY); + } + + @Override + public void addGenresToFilm(Film film) { + String sql = "INSERT INTO films_genres(film_id, genre_id) VALUES(?,?)"; + Long filmId = film.getId(); + + List batchArgs = film.getGenres() + .stream() + .map(genre -> new Object[]{filmId, genre.getId()}) + .toList(); + + jdbc.batchUpdate(sql, batchArgs); + } + + @Override + public void updateFilmGenres(Film film) { + String sql = "DELETE FROM films_genres WHERE film_id = ?"; + jdbc.update(sql, film.getId()); + addGenresToFilm(film); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/db/DbMpaStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/db/DbMpaStorage.java new file mode 100644 index 0000000..514224c --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/db/DbMpaStorage.java @@ -0,0 +1,38 @@ +package ru.yandex.practicum.filmorate.storage.film.db; + +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.Mpa; +import ru.yandex.practicum.filmorate.storage.BaseDao; +import ru.yandex.practicum.filmorate.storage.film.MpaStorage; +import ru.yandex.practicum.filmorate.storage.mappers.MpaMapper; + +import java.util.Collection; +import java.util.Optional; + +@Repository("DbMpaStorage") +public class DbMpaStorage extends BaseDao implements MpaStorage { + + private static final String SELECT_BY_ID_QUERY = "SELECT * FROM mpa WHERE mpa_id = ?"; + private static final String SELECT_ALL_QUERY = "SELECT * FROM mpa;"; + + public DbMpaStorage(JdbcTemplate jdbc, MpaMapper mapper) { + super(jdbc, mapper); + } + + @Override + public Optional getMpa(Long id) { + try { + Mpa mpa = get(SELECT_BY_ID_QUERY, id); + return Optional.ofNullable(mpa); + } catch (EmptyResultDataAccessException e) { + return Optional.empty(); + } + } + + @Override + public Collection getAll() { + return getAll(SELECT_ALL_QUERY); + } +} From 0fbd8a3446cf10e860d020c307f3e655cdb44508 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:07:27 +0300 Subject: [PATCH 07/26] feat: add Film and Like db storages --- .../filmorate/storage/film/FilmStorage.java | 3 + .../filmorate/storage/film/LikeStorage.java | 5 +- .../storage/film/db/DbFilmStorage.java | 120 ++++++++++++++++++ .../storage/film/db/DbLikeStorage.java | 33 +++++ 4 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/film/db/DbFilmStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/film/db/DbLikeStorage.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java index 6e55005..ed5cc24 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java @@ -3,5 +3,8 @@ import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.storage.BasicStorage; +import java.util.Collection; + public interface FilmStorage extends BasicStorage { + Collection getTopFilms(Long count); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/LikeStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/LikeStorage.java index c2cc79e..8ba7338 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/LikeStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/LikeStorage.java @@ -2,9 +2,8 @@ public interface LikeStorage { - void initializeLikesSet(Long id); - - void clearLikesSet(Long id); + default void clearLikes(Long id) { + } Long addLike(Long filmId, Long userId); diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/db/DbFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/db/DbFilmStorage.java new file mode 100644 index 0000000..ea83281 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/db/DbFilmStorage.java @@ -0,0 +1,120 @@ +package ru.yandex.practicum.filmorate.storage.film.db; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.storage.BaseDao; +import ru.yandex.practicum.filmorate.storage.film.FilmStorage; +import ru.yandex.practicum.filmorate.storage.mappers.FilmMapper; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +@Repository("DbFilmStorage") +public class DbFilmStorage extends BaseDao implements FilmStorage { + + private static final String SELECT_FIELDS = """ + SELECT f.*, + COUNT(l.film_id) AS likes, + m.mpa_id, + m.name AS mpa_name, + ARRAY_AGG(g.genre_id) AS genre_ids, + ARRAY_AGG(g.name) AS genre_names + """; + + private static final String FROM_JOIN_EVERYTHING = """ + FROM films AS f + LEFT JOIN mpa AS m ON f.mpa_id = m.mpa_id + LEFT JOIN likes AS l ON f.film_id = l.film_id + LEFT JOIN films_genres AS fg ON f.film_id = fg.film_id + LEFT JOIN genres AS g ON fg.genre_id = g.genre_id"""; + + private static final String GROUP_BY = "GROUP BY f.film_id"; + private static final String EXISTS_QUERY = "SELECT EXISTS(SELECT 1 FROM films WHERE film_id = ?)"; + private static final String DELETE_QUERY = "DELETE FROM films WHERE film_id = ?"; + + private static final String SELECT_BY_ID_QUERY = String.format("%s %s WHERE f.film_id = ? %s", + SELECT_FIELDS, FROM_JOIN_EVERYTHING, GROUP_BY); + + private static final String SELECT_ALL_QUERY = String.format("%s %s %s", + SELECT_FIELDS, FROM_JOIN_EVERYTHING, GROUP_BY); + + private static final String UPDATE_QUERY = """ + UPDATE films + SET name = :name, description = :description, + release_date = :release_date, duration = :duration, mpa_id = :mpa_id + WHERE film_id = :film_id"""; + + + private final SimpleJdbcInsert simpleInsert; + + public DbFilmStorage(JdbcTemplate jdbc, FilmMapper mapper) { + super(jdbc, mapper); + this.simpleInsert = new SimpleJdbcInsert(jdbc) + .withTableName("films") + .usingGeneratedKeyColumns("film_id"); + } + + @Override + public Film add(Film film) { + Long id = simpleInsert.executeAndReturnKey(filmToMap(film)).longValue(); + film.setId(id); + + return film; + } + + @Override + public Film update(Film film) { + Map map = filmToMap(film); + map.put("film_id", film.getId()); + + update(UPDATE_QUERY, map); + + return film; + } + + @Override + public Film remove(Long id) { + Film film = get(id); + remove(DELETE_QUERY, id); + return film; + } + + @Override + public Film get(Long id) { + return get(SELECT_BY_ID_QUERY, id); + } + + @Override + public Collection getAll() { + return getAll(SELECT_ALL_QUERY); + } + + @Override + public boolean contains(Long id) { + return jdbc.queryForObject(EXISTS_QUERY, Boolean.class, id); + } + + @Override + public Collection getTopFilms(Long count) { + String sql = SELECT_ALL_QUERY + " ORDER BY likes DESC LIMIT ?"; + + return jdbc.query(sql, mapper, count); + } + + private Map filmToMap(Film film) { + Map map = new HashMap<>(); + map.put("name", film.getName()); + map.put("description", film.getDescription()); + map.put("release_date", film.getReleaseDate()); + map.put("duration", film.getDuration()); + + if (film.getMpa() != null) { + map.put("mpa_id", film.getMpa().getId()); + } + + return map; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/db/DbLikeStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/db/DbLikeStorage.java new file mode 100644 index 0000000..9221172 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/db/DbLikeStorage.java @@ -0,0 +1,33 @@ +package ru.yandex.practicum.filmorate.storage.film.db; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.storage.film.LikeStorage; + +@Repository("DbLikeStorage") +public class DbLikeStorage implements LikeStorage { + + private final JdbcTemplate jdbc; + + public DbLikeStorage(JdbcTemplate jdbc) { + this.jdbc = jdbc; + } + + @Override + public Long addLike(Long filmId, Long userId) { + String insertSql = "INSERT INTO likes(film_id, user_id) VALUES(?, ?)"; + String countSql = "SELECT COUNT(*) FROM likes WHERE film_id = ?"; + jdbc.update(insertSql, filmId, userId); + + return jdbc.queryForObject(countSql, Long.class, filmId); + } + + @Override + public Long deleteLike(Long filmId, Long userId) { + String deleteSql = "DELETE FROM likes WHERE user_id = ?"; + String countSql = "SELECT COUNT(*) FROM likes WHERE film_id = ?"; + + jdbc.update(deleteSql, userId); + return jdbc.queryForObject(countSql, Long.class, filmId); + } +} From 07fad9cb3348ee4ea43f0fa00304060466c44a0f Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:07:45 +0300 Subject: [PATCH 08/26] feat: add User and FriendShip db storages --- .../storage/user/FriendShipStorage.java | 7 +- .../filmorate/storage/user/UserStorage.java | 4 + .../storage/user/db/DbFriendShipStorage.java | 47 ++++++++++ .../storage/user/db/DbUserStorage.java | 91 +++++++++++++++++++ 4 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/user/db/DbFriendShipStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/user/db/DbUserStorage.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/FriendShipStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/FriendShipStorage.java index d54df19..af50ddc 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/FriendShipStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/FriendShipStorage.java @@ -4,13 +4,14 @@ public interface FriendShipStorage { - void initializeFriendsSet(Long id); - - void clearFriendsSet(Long id); + default void deleteUserFromAllFriends(Long id) { + } void addFriend(Long senderId, Long receiverId); void deleteFriend(Long senderId, Long receiverId); Set getFriends(Long id); + + Set getCommonFriends(Long id, Long otherId); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java index ae4c687..0377f5d 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java @@ -3,5 +3,9 @@ import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.storage.BasicStorage; +import java.util.Collection; +import java.util.List; + public interface UserStorage extends BasicStorage { + List getAllFromCollection(Collection ids); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/db/DbFriendShipStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/db/DbFriendShipStorage.java new file mode 100644 index 0000000..728af3e --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/db/DbFriendShipStorage.java @@ -0,0 +1,47 @@ +package ru.yandex.practicum.filmorate.storage.user.db; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.storage.user.FriendShipStorage; + +import java.util.Set; + +@Repository("DbFriendShipStorage") +public class DbFriendShipStorage implements FriendShipStorage { + + private static final String SELECT_BY_ID_QUERY = "SELECT friend_id FROM friendships WHERE user_id = ?"; + private static final String INSERT_QUERY = "INSERT INTO friendships(user_id, friend_id) VALUES(?,?)"; + private static final String DELETE_QUERY = "DELETE FROM friendships WHERE user_id = ? AND friend_id = ?"; + private static final String SELECT_COMMON_FRIENDS = """ + SELECT f1.friend_id + FROM friendships f1 + JOIN friendships f2 ON f1.friend_id = f2.friend_id + WHERE f1.user_id = ? AND f2.user_id = ?"""; + + + private final JdbcTemplate jdbc; + + public DbFriendShipStorage(JdbcTemplate jdbc) { + this.jdbc = jdbc; + } + + @Override + public void addFriend(Long senderId, Long receiverId) { + jdbc.update(INSERT_QUERY, senderId, receiverId); + } + + @Override + public void deleteFriend(Long senderId, Long receiverId) { + jdbc.update(DELETE_QUERY, senderId, receiverId); + } + + @Override + public Set getFriends(Long id) { + return Set.copyOf(jdbc.queryForList(SELECT_BY_ID_QUERY, Long.class, id)); + } + + @Override + public Set getCommonFriends(Long id, Long otherId) { + return Set.copyOf(jdbc.queryForList(SELECT_COMMON_FRIENDS, Long.class, id, otherId)); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/db/DbUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/db/DbUserStorage.java new file mode 100644 index 0000000..c62e56c --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/db/DbUserStorage.java @@ -0,0 +1,91 @@ +package ru.yandex.practicum.filmorate.storage.user.db; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.BaseDao; +import ru.yandex.practicum.filmorate.storage.mappers.UserMapper; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; + +import java.util.*; + +@Repository("DbUserStorage") +public class DbUserStorage extends BaseDao implements UserStorage { + + private static final String SELECT_BY_ID_QUERY = "SELECT * FROM users WHERE user_id = ?"; + private static final String SELECT_ALL_QUERY = "SELECT * FROM users;"; + private static final String DELETE_QUERY = "DELETE FROM users WHERE user_id = ?"; + private static final String EXISTS_QUERY = "SELECT EXISTS(SELECT 1 FROM users WHERE user_id = ?)"; + private static final String SELECT_ALL_FROM_COLLECTION = "SELECT * FROM users WHERE user_id IN (:ids)"; + private static final String UPDATE_QUERY = """ + UPDATE users SET email = :email, login = :login, name = :name, birthday = :birthday + WHERE user_id = :user_id + """; + + private final SimpleJdbcInsert simpleInsert; + + public DbUserStorage(JdbcTemplate jdbc, UserMapper mapper) { + super(jdbc, mapper); + this.simpleInsert = new SimpleJdbcInsert(jdbc) + .withTableName("users") + .usingGeneratedKeyColumns("user_id"); + } + + @Override + public User add(User user) { + Long id = simpleInsert.executeAndReturnKey(userToMap(user)).longValue(); + user.setId(id); + + return user; + } + + @Override + public User update(User user) { + Map map = userToMap(user); + map.put("user_id", user.getId()); + update(UPDATE_QUERY, map); + return user; + } + + @Override + public User remove(Long id) { + User user = get(id); + remove(DELETE_QUERY, id); + return user; + } + + @Override + public User get(Long id) { + return get(SELECT_BY_ID_QUERY, id); + } + + @Override + public Collection getAll() { + return getAll(SELECT_ALL_QUERY); + } + + @Override + public boolean contains(Long id) { + return jdbc.queryForObject(EXISTS_QUERY, Boolean.class, id); + } + + @Override + public List getAllFromCollection(Collection ids) { + if (ids == null || ids.isEmpty()) { + return Collections.emptyList(); + } + + return namedJdbc.query(SELECT_ALL_FROM_COLLECTION, Collections.singletonMap("ids", ids), mapper); + } + + private Map userToMap(User user) { + Map map = new HashMap<>(); + map.put("email", user.getEmail()); + map.put("login", user.getLogin()); + map.put("name", user.getName()); + map.put("birthday", user.getBirthday()); + + return map; + } +} From b32f90fb71805317976bb99cadf1fc4566bf407b Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:08:14 +0300 Subject: [PATCH 09/26] fix: deleted size() method --- .../ru/yandex/practicum/filmorate/storage/BasicStorage.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/BasicStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/BasicStorage.java index 0d5f7d7..ca99808 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/BasicStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/BasicStorage.java @@ -16,6 +16,4 @@ public interface BasicStorage { boolean contains(Long id); - int size(); - } From 7f04be8e18bf89f0395260bf182c975930d8a2cc Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:08:33 +0300 Subject: [PATCH 10/26] feat: add row mappers for models --- .../filmorate/storage/mappers/FilmMapper.java | 70 +++++++++++++++++++ .../storage/mappers/GenreMapper.java | 20 ++++++ .../filmorate/storage/mappers/MpaMapper.java | 20 ++++++ .../filmorate/storage/mappers/UserMapper.java | 28 ++++++++ 4 files changed, 138 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/mappers/FilmMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/mappers/GenreMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/mappers/MpaMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/mappers/UserMapper.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/mappers/FilmMapper.java b/src/main/java/ru/yandex/practicum/filmorate/storage/mappers/FilmMapper.java new file mode 100644 index 0000000..d5f8359 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/mappers/FilmMapper.java @@ -0,0 +1,70 @@ +package ru.yandex.practicum.filmorate.storage.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.Genre; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.sql.Array; +import java.sql.Date; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; + +@Component +public class FilmMapper implements RowMapper { + + @Override + public Film mapRow(ResultSet resultSet, int rowNum) throws SQLException { + Film film = new Film(); + film.setId(resultSet.getLong("film_id")); + film.setName(resultSet.getString("name")); + film.setDescription(resultSet.getString("description")); + film.setLikes(resultSet.getLong("likes")); + film.setDuration(resultSet.getInt("duration")); + + Date date = resultSet.getDate("release_date"); + LocalDate releaseDate = date == null ? null : date.toLocalDate(); + + film.setReleaseDate(releaseDate); + + mapMpa(film, resultSet); + mapGenres(film, resultSet); + + return film; + } + + private void mapMpa(Film film, ResultSet resultSet) throws SQLException { + Long mpaId = resultSet.getLong("mpa_id"); + if (!resultSet.wasNull()) { + String mpaName = resultSet.getString("mpa_name"); + film.setMpa(new Mpa(mpaId, mpaName)); + } + } + + private void mapGenres(Film film, ResultSet resultSet) throws SQLException { + Array sqlArrayIds = resultSet.getArray("genre_ids"); + if (resultSet.wasNull()) { + return; + } + + Array sqlArrayNames = resultSet.getArray("genre_names"); + + Object[] idsArray = (Object[]) sqlArrayIds.getArray(); + Object[] namesArray = (Object[]) sqlArrayNames.getArray(); + + Set genres = new HashSet<>(); + + for (int i = 0; i < idsArray.length; i++) { + if (idsArray[i] != null && namesArray[i] != null) { + Long id = ((Number) idsArray[i]).longValue(); + String name = namesArray[i].toString(); + genres.add(new Genre(id, name)); + } + } + film.setGenres(genres); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/mappers/GenreMapper.java b/src/main/java/ru/yandex/practicum/filmorate/storage/mappers/GenreMapper.java new file mode 100644 index 0000000..2554b4d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/mappers/GenreMapper.java @@ -0,0 +1,20 @@ +package ru.yandex.practicum.filmorate.storage.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 GenreMapper implements RowMapper { + @Override + public Genre mapRow(ResultSet resultSet, int rowNum) throws SQLException { + Genre genre = new Genre(); + genre.setId(resultSet.getLong("genre_id")); + genre.setName(resultSet.getString("name")); + + return genre; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/mappers/MpaMapper.java b/src/main/java/ru/yandex/practicum/filmorate/storage/mappers/MpaMapper.java new file mode 100644 index 0000000..39bc681 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/mappers/MpaMapper.java @@ -0,0 +1,20 @@ +package ru.yandex.practicum.filmorate.storage.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 MpaMapper implements RowMapper { + @Override + public Mpa mapRow(ResultSet resultSet, int rowNum) throws SQLException { + Mpa mpa = new Mpa(); + mpa.setId(resultSet.getLong("mpa_id")); + mpa.setName(resultSet.getString("name")); + + return mpa; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/mappers/UserMapper.java b/src/main/java/ru/yandex/practicum/filmorate/storage/mappers/UserMapper.java new file mode 100644 index 0000000..4dc4a93 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/mappers/UserMapper.java @@ -0,0 +1,28 @@ +package ru.yandex.practicum.filmorate.storage.mappers; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.User; + +import java.sql.Date; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; + +@Component +public class UserMapper implements RowMapper { + @Override + public User mapRow(ResultSet resultSet, int rowNum) throws SQLException { + User user = new User(); + user.setId(resultSet.getLong("user_id")); + user.setEmail(resultSet.getString("email")); + user.setLogin(resultSet.getString("login")); + user.setName(resultSet.getString("name")); + + Date date = resultSet.getDate("birthday"); + LocalDate birthday = date == null ? null : date.toLocalDate(); + user.setBirthday(birthday); + + return user; + } +} \ No newline at end of file From b64b3c3884731e5a232b3edf386437d1ed9c4861 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:09:16 +0300 Subject: [PATCH 11/26] fix: moved in memory storages and refactored logic --- .../{ => memory}/InMemoryFilmStorage.java | 19 ++++--- .../{ => memory}/InMemoryLikeStorage.java | 20 ++++---- .../user/InMemoryFriendShipStorage.java | 43 ---------------- .../memory/InMemoryFriendShipStorage.java | 50 +++++++++++++++++++ .../{ => memory}/InMemoryUserStorage.java | 9 ++-- 5 files changed, 76 insertions(+), 65 deletions(-) rename src/main/java/ru/yandex/practicum/filmorate/storage/film/{ => memory}/InMemoryFilmStorage.java (76%) rename src/main/java/ru/yandex/practicum/filmorate/storage/film/{ => memory}/InMemoryLikeStorage.java (53%) delete mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryFriendShipStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/user/memory/InMemoryFriendShipStorage.java rename src/main/java/ru/yandex/practicum/filmorate/storage/user/{ => memory}/InMemoryUserStorage.java (85%) diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/memory/InMemoryFilmStorage.java similarity index 76% rename from src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java rename to src/main/java/ru/yandex/practicum/filmorate/storage/film/memory/InMemoryFilmStorage.java index 82b593c..b7183d7 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/memory/InMemoryFilmStorage.java @@ -1,10 +1,13 @@ -package ru.yandex.practicum.filmorate.films.film; +package ru.yandex.practicum.filmorate.storage.film.memory; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Repository; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.storage.film.FilmStorage; -import java.util.*; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; @@ -18,7 +21,7 @@ * @see Film * @see FilmStorage */ -@Component +@Repository("MemFilmStorage") public class InMemoryFilmStorage implements FilmStorage { private final Map films; @@ -33,7 +36,6 @@ public InMemoryFilmStorage() { public Film add(Film film) { film.setId(idGenerator.getAndIncrement()); films.put(film.getId(), film); - return films.get(film.getId()); } @@ -65,7 +67,10 @@ public boolean contains(Long id) { } @Override - public int size() { - return films.size(); + public Collection getTopFilms(Long count) { + return films.values().stream() + .sorted(Comparator.comparingLong(Film::getLikes).reversed()) + .limit(count) + .toList(); } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryLikeStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/memory/InMemoryLikeStorage.java similarity index 53% rename from src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryLikeStorage.java rename to src/main/java/ru/yandex/practicum/filmorate/storage/film/memory/InMemoryLikeStorage.java index 5cf8a84..a254989 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryLikeStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/memory/InMemoryLikeStorage.java @@ -1,13 +1,15 @@ -package ru.yandex.practicum.filmorate.storage.film; +package ru.yandex.practicum.filmorate.storage.film.memory; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.storage.film.LikeStorage; +import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -@Component +@Repository("MemLikeStorage") public class InMemoryLikeStorage implements LikeStorage { private final Map> likes; @@ -16,27 +18,23 @@ public InMemoryLikeStorage() { this.likes = new ConcurrentHashMap<>(); } - @Override - public void initializeLikesSet(Long id) { - likes.put(id, new HashSet<>()); - } @Override - public void clearLikesSet(Long id) { + public void clearLikes(Long id) { likes.remove(id); } @Override public Long addLike(Long filmId, Long userId) { - likes.get(filmId).add(userId); + likes.computeIfAbsent(filmId, id -> new HashSet<>()).add(userId); return (long) likes.get(filmId).size(); } @Override public Long deleteLike(Long filmId, Long userId) { - likes.get(filmId).remove(userId); + likes.getOrDefault(filmId, Collections.emptySet()).remove(userId); - return (long) likes.get(filmId).size(); + return (long) likes.getOrDefault(filmId, Collections.emptySet()).size(); } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryFriendShipStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryFriendShipStorage.java deleted file mode 100644 index 88cb81d..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryFriendShipStorage.java +++ /dev/null @@ -1,43 +0,0 @@ -package ru.yandex.practicum.filmorate.storage.user; - -import org.springframework.stereotype.Component; - -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -@Component -public class InMemoryFriendShipStorage implements FriendShipStorage { - - private final Map> friendships; - - public InMemoryFriendShipStorage() { - this.friendships = new ConcurrentHashMap<>(); - } - - @Override - public void initializeFriendsSet(Long id) { - friendships.put(id, new HashSet<>()); - } - - @Override - public void clearFriendsSet(Long id) { - friendships.remove(id); - } - - @Override - public void addFriend(Long senderId, Long receiverId) { - friendships.get(senderId).add(receiverId); - } - - @Override - public void deleteFriend(Long senderId, Long receiverId) { - friendships.get(senderId).remove(receiverId); - } - - @Override - public Set getFriends(Long id) { - return friendships.get(id); - } -} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/memory/InMemoryFriendShipStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/memory/InMemoryFriendShipStorage.java new file mode 100644 index 0000000..2a1c7d3 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/memory/InMemoryFriendShipStorage.java @@ -0,0 +1,50 @@ +package ru.yandex.practicum.filmorate.storage.user.memory; + +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.storage.user.FriendShipStorage; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +@Repository("MemFriendShipStorage") +public class InMemoryFriendShipStorage implements FriendShipStorage { + + private final Map> friendships; + + public InMemoryFriendShipStorage() { + this.friendships = new ConcurrentHashMap<>(); + } + + @Override + public void deleteUserFromAllFriends(Long id) { + friendships.get(id) + .forEach(friendId -> friendships.getOrDefault(friendId, Collections.emptySet()).remove(id)); + friendships.remove(id); + } + + @Override + public void addFriend(Long senderId, Long receiverId) { + friendships.computeIfAbsent(senderId, id -> new HashSet<>()).add(receiverId); + } + + @Override + public void deleteFriend(Long senderId, Long receiverId) { + friendships.getOrDefault(senderId, Collections.emptySet()).remove(receiverId); + } + + @Override + public Set getFriends(Long id) { + return friendships.getOrDefault(id, Collections.emptySet()); + } + + @Override + public Set getCommonFriends(Long id, Long otherId) { + Set friends1 = getFriends(id); + Set friends2 = getFriends(otherId); + friends1.retainAll(friends2); + return friends1; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/memory/InMemoryUserStorage.java similarity index 85% rename from src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java rename to src/main/java/ru/yandex/practicum/filmorate/storage/user/memory/InMemoryUserStorage.java index 4b26fae..7944882 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/memory/InMemoryUserStorage.java @@ -1,7 +1,8 @@ -package ru.yandex.practicum.filmorate.storage.user; +package ru.yandex.practicum.filmorate.storage.user.memory; import org.springframework.stereotype.Component; import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; import java.util.Collection; import java.util.List; @@ -19,7 +20,7 @@ * @see User * @see UserStorage */ -@Component +@Component("MemUserStorage") public class InMemoryUserStorage implements UserStorage { private final Map users; @@ -66,7 +67,7 @@ public boolean contains(Long id) { } @Override - public int size() { - return users.size(); + public List getAllFromCollection(Collection ids) { + return ids.stream().map(users::get).toList(); } } From 20d593fa22fc056a97bce469d545cc6594605752 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:09:34 +0300 Subject: [PATCH 12/26] feat: add BaseDao to help dbStorages --- .../practicum/filmorate/storage/BaseDao.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/BaseDao.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/BaseDao.java b/src/main/java/ru/yandex/practicum/filmorate/storage/BaseDao.java new file mode 100644 index 0000000..19057ce --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/BaseDao.java @@ -0,0 +1,36 @@ +package ru.yandex.practicum.filmorate.storage; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +import java.util.Collection; +import java.util.Map; + +public abstract class BaseDao { + protected final JdbcTemplate jdbc; + protected final NamedParameterJdbcTemplate namedJdbc; + protected final RowMapper mapper; + + public BaseDao(JdbcTemplate jdbc, RowMapper mapper) { + this.jdbc = jdbc; + this.namedJdbc = new NamedParameterJdbcTemplate(jdbc); + this.mapper = mapper; + } + + public void update(String sql, Map map) { + namedJdbc.update(sql, map); + } + + public void remove(String sql, Object... params) { + jdbc.update(sql, params); + } + + public T get(String sql, Object... params) { + return jdbc.queryForObject(sql, mapper, params); + } + + public Collection getAll(String sql, Object... params) { + return jdbc.query(sql, mapper, params); + } +} From d57054f87a090b25416d956892a53225c9ac9025 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:10:09 +0300 Subject: [PATCH 13/26] fix: updated services to work with db and inMemory --- .../filmorate/service/film/FilmService.java | 48 ++++++++++++------- .../filmorate/service/user/UserService.java | 33 ++++--------- 2 files changed, 42 insertions(+), 39 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java index b53d904..1b98a88 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java @@ -1,15 +1,17 @@ package ru.yandex.practicum.filmorate.service.film; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.storage.film.FilmStorage; +import ru.yandex.practicum.filmorate.storage.film.GenreStorage; import ru.yandex.practicum.filmorate.storage.film.LikeStorage; import ru.yandex.practicum.filmorate.storage.user.UserStorage; import java.util.Collection; -import java.util.Comparator; /** * Main service for film operations and business logic. @@ -22,19 +24,25 @@ * @see FilmStorage */ @Service +@Transactional public class FilmService implements FilmServiceInterface { private final FilmStorage filmStorage; private final UserStorage userStorage; private final LikeStorage likeStorage; + private final GenreStorage genreStorage; /** * Constructor for dependency injection */ - public FilmService(FilmStorage filmStorage, UserStorage userStorage, LikeStorage likeStorage) { + public FilmService(@Qualifier("DbFilmStorage") FilmStorage filmStorage, + @Qualifier("DbUserStorage") UserStorage userStorage, + @Qualifier("DbLikeStorage") LikeStorage likeStorage, + @Qualifier("DbGenreStorage") GenreStorage genreStorage) { this.filmStorage = filmStorage; this.userStorage = userStorage; this.likeStorage = likeStorage; + this.genreStorage = genreStorage; } /** @@ -68,9 +76,13 @@ public Collection getAllFilms() { */ @Override public Film addFilm(Film film) { - Film returnFilm = filmStorage.add(film); - likeStorage.initializeLikesSet(returnFilm.getId()); - return returnFilm; + film = filmStorage.add(film); + + if (film.getGenres() != null && !film.getGenres().isEmpty()) { + genreStorage.addGenresToFilm(film); + } + + return film; } /** @@ -87,7 +99,13 @@ public Film updateFilm(Film film) { } throwIfNotFound(film.getId()); - return filmStorage.update(film); + film = filmStorage.update(film); + + if (film.getGenres() != null && !film.getGenres().isEmpty()) { + genreStorage.updateFilmGenres(film); + } + + return film; } /** @@ -100,7 +118,7 @@ public Film updateFilm(Film film) { public void deleteFilm(Long id) { throwIfNotFound(id); filmStorage.remove(id); - likeStorage.clearLikesSet(id); + likeStorage.clearLikes(id); } /** @@ -119,9 +137,10 @@ public Film addLike(Long filmId, Long userId) { } Long newLikes = likeStorage.addLike(filmId, userId); - filmStorage.get(filmId).setLikes(newLikes); + Film film = filmStorage.get(filmId); + film.setLikes(newLikes); - return filmStorage.get(filmId); + return film; } /** @@ -140,9 +159,10 @@ public Film removeLike(Long filmId, Long userId) { } Long newLikes = likeStorage.deleteLike(filmId, userId); - filmStorage.get(filmId).setLikes(newLikes); + Film film = filmStorage.get(filmId); + film.setLikes(newLikes); - return filmStorage.get(filmId); + return film; } /** @@ -153,11 +173,7 @@ public Film removeLike(Long filmId, Long userId) { */ @Override public Collection getTopFilms(Long count) { - - return filmStorage.getAll().stream() - .sorted(Comparator.comparingLong(Film::getLikes).reversed()) - .limit(count) - .toList(); + return filmStorage.getTopFilms(count); } @Override diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java index eff4749..a827a79 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java @@ -1,6 +1,8 @@ package ru.yandex.practicum.filmorate.service.user; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.User; @@ -22,6 +24,7 @@ * @see UserStorage */ @Service +@Transactional public class UserService implements UserServiceInterface { private final UserStorage userStorage; @@ -30,7 +33,8 @@ public class UserService implements UserServiceInterface { /** * Constructor for dependency injection */ - public UserService(UserStorage userStorage, FriendShipStorage friendShipStorage) { + public UserService(@Qualifier("DbUserStorage") UserStorage userStorage, + @Qualifier("DbFriendShipStorage") FriendShipStorage friendShipStorage) { this.userStorage = userStorage; this.friendShipStorage = friendShipStorage; } @@ -69,10 +73,7 @@ public User addUser(User user) { user.setName(user.getLogin()); } - User returnUser = userStorage.add(user); - friendShipStorage.initializeFriendsSet(returnUser.getId()); - - return returnUser; + return userStorage.add(user); } /** @@ -103,11 +104,7 @@ public User updateUser(User user) { @Override public void deleteUser(Long id) { throwIfNotFound(id); - - Set friendIds = friendShipStorage.getFriends(id); - friendIds.forEach(friendId -> friendShipStorage.getFriends(friendId).remove(id)); - friendShipStorage.clearFriendsSet(id); - + friendShipStorage.deleteUserFromAllFriends(id); userStorage.remove(id); } @@ -129,7 +126,6 @@ public void addFriend(Long senderId, Long receiverId) { throwIfNotFound(receiverId); friendShipStorage.addFriend(senderId, receiverId); - friendShipStorage.addFriend(receiverId, senderId); } /** @@ -146,7 +142,6 @@ public void deleteFriend(Long senderId, Long receiverId) { throwIfNotFound(receiverId); friendShipStorage.deleteFriend(senderId, receiverId); - friendShipStorage.deleteFriend(receiverId, senderId); } /** @@ -157,10 +152,7 @@ public void deleteFriend(Long senderId, Long receiverId) { @Override public Collection getFriends(Long id) { throwIfNotFound(id); - - return friendShipStorage.getFriends(id).stream() - .map(userStorage::get) - .toList(); + return userStorage.getAllFromCollection(friendShipStorage.getFriends(id)); } /** @@ -176,13 +168,8 @@ public Collection getCommonFriends(Long id, Long otherId) { throwIfNotFound(id); throwIfNotFound(otherId); - Set friends = friendShipStorage.getFriends(id); - Set otherFriends = friendShipStorage.getFriends(otherId); - - return friends.stream() - .filter(otherFriends::contains) - .map(userStorage::get) - .toList(); + Set ids = friendShipStorage.getCommonFriends(id, otherId); + return userStorage.getAllFromCollection(ids); } @Override From c640ee8942fac54b46b95d347b152d0465dd9342 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:10:38 +0300 Subject: [PATCH 14/26] fix: add new field to Film --- src/main/java/ru/yandex/practicum/filmorate/model/Film.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 6fe8c60..6b59539 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -9,6 +9,8 @@ import ru.yandex.practicum.filmorate.validation.ValidReleaseDate; import java.time.LocalDate; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; /** @@ -54,4 +56,8 @@ public Film(Long id, String name, String description, LocalDate releaseDate, Int @JsonProperty(access = JsonProperty.Access.READ_ONLY) private Long likes = 0L; + + private Mpa mpa; + + private Set genres = ConcurrentHashMap.newKeySet(); } From 4dcacb4bb5853649a86dec7c6fd23c3d4b46d472 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:10:52 +0300 Subject: [PATCH 15/26] fix: add DataIntegrityViolationException handler --- .../exception/GlobalExceptionHandler.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/GlobalExceptionHandler.java b/src/main/java/ru/yandex/practicum/filmorate/exception/GlobalExceptionHandler.java index 42ce9ce..ad5a985 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/exception/GlobalExceptionHandler.java +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/GlobalExceptionHandler.java @@ -5,6 +5,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -102,6 +103,33 @@ public ResponseEntity handleMismatchAndConstraintViolation( return createBadRequest(request.getRequestURI(), Map.of("error", "Invalid request format")); } + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity handleDataIntegrityViolationException( + DataIntegrityViolationException ex, + HttpServletRequest request) { + + logInfo(ex, "Handling DataIntegrityViolationException"); + String msg = ex.getMessage().toLowerCase(); + HttpStatus status = HttpStatus.BAD_REQUEST; + String errorMsg = "Invalid request format"; + + if (msg.contains("foreign key")) { + + if (msg.contains("mpa")) { + errorMsg = "Mpa не найден"; + status = HttpStatus.NOT_FOUND; + } else if (msg.contains("genre")) { + errorMsg = "Один из указанных жанров не найден"; + status = HttpStatus.NOT_FOUND; + } + } + + return createResponseEntity( + status, + request.getRequestURI(), + Map.of("error", errorMsg)); + } + /** * Helper for HttpMessageNotReadableException handler *

Extracts meaningful error messages from JSON parsing exceptions. From db862b07968b30e09b1f310ce12c6a4de89c2630 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:11:26 +0300 Subject: [PATCH 16/26] fix: add new methods to controllers --- .../filmorate/controller/FilmController.java | 23 +++++++++++++++++++ .../filmorate/controller/UserController.java | 23 +++++++++++++++++++ 2 files changed, 46 insertions(+) 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 e490cb5..a79575f 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -36,6 +36,18 @@ public Collection getFilms() { return filmService.getAllFilms(); } + /** + * Handles GET method. + *

Retrieves film from the storage. + * + * @param id film`s id. Must be positive number + * @return Film. + */ + @GetMapping("/{id}") + public Film getFilm(@PathVariable @Positive Long id) { + return filmService.getFilm(id); + } + /** * Handles POST method. *

Creates film in storage after validation. @@ -70,6 +82,17 @@ public Film updateFilm(@Valid @RequestBody Film newFilm) { return filmService.updateFilm(newFilm); } + /** + * Handles DELETE method. + *

Deletes film from the storage. + * + * @param id film`s id. Must be positive number + */ + @DeleteMapping("/{id}") + public void deleteFilm(@PathVariable @Positive Long id) { + filmService.deleteFilm(id); + } + /** * Handles GET method. *

Retrieves top {@code count} popular films based on likes. 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 975e081..5241b6c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -37,6 +37,18 @@ public Collection getUsers() { return userService.getAllUsers(); } + /** + * Handles GET method. + *

Retrieves user from the storage. + * + * @param id user`s id. Must be positive number + * @return User. + */ + @GetMapping("/{id}") + public User getUser(@PathVariable @Positive Long id) { + return userService.getUser(id); + } + /** * Handles POST method. *

Creates user in storage after validation. @@ -67,6 +79,17 @@ public User updateUser(@Valid @RequestBody User newUser) { return userService.updateUser(newUser); } + /** + * Handles DELETE method. + *

Deletes user from the storage. + * + * @param id user`s id. Must be positive number + */ + @DeleteMapping("/{id}") + public void deleteUser(@PathVariable @Positive Long id) { + userService.deleteUser(id); + } + /** * Handles GET method. *

Return collection of user`s friends. From 7feef8c35af4db7444d97a7d8fc29971d058107d Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:11:46 +0300 Subject: [PATCH 17/26] feat: add sql config files for testdb --- src/test/resources/data.sql | 51 +++++++++++++++++++++++++++++++++++ src/test/resources/schema.sql | 44 ++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 src/test/resources/data.sql create mode 100644 src/test/resources/schema.sql diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql new file mode 100644 index 0000000..4019ea2 --- /dev/null +++ b/src/test/resources/data.sql @@ -0,0 +1,51 @@ +INSERT INTO mpa (name) VALUES +('G'), +('PG'), +('PG-13'), +('R'), +('NC-17'); + +INSERT INTO genres (name) VALUES +('Комедия'), +('Драма'), +('Мультфильм'), +('Триллер'), +('Документальный'), +('Боевик'); + +INSERT INTO users (email, login, name, birthday) VALUES +('email1', 'login1', 'name1', '2000-01-01'), +('email2', 'login2', 'name2', '2000-01-01'), +('email3', 'login3', 'name3', '2000-01-01'), +('email4', 'login4', 'name4', '2000-01-01'), +('email5', 'login5', 'name5', NULL); + +INSERT INTO friendships(user_id, friend_id) VALUES +(1L, 2L), +(2L, 1L), +(3L, 1L), +(3L, 4L); + +INSERT INTO films (name, description, release_date, duration, mpa_id) VALUES +('name1', 'desc1', '2000-01-01', 100, 1), +('name2', 'desc2', '2000-01-01', 100, 2), +('name3', 'desc3', '2000-01-01', 100, 3), +('name4', 'desc4', '2000-01-01', 100, 4), +('name5', 'desc5', '2000-01-01', 100, 5); + +INSERT INTO films_genres (film_id, genre_id) VALUES +(4,1), +(5,1), +(5,2), +(5,3); + +INSERT INTO likes (film_id, user_id) VALUES +(1,1), +(1,2), +(2,1), +(2,3), +(2,4), +(3,1), +(3,2), +(3,3), +(3,4); \ No newline at end of file diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql new file mode 100644 index 0000000..a1597a0 --- /dev/null +++ b/src/test/resources/schema.sql @@ -0,0 +1,44 @@ +CREATE TABLE IF NOT EXISTS users ( + user_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + email VARCHAR NOT NULL, + login VARCHAR NOT NULL, + name VARCHAR, + birthday DATE); + +CREATE TABLE IF NOT EXISTS friendships ( + user_id BIGINT, + friend_id BIGINT, + FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, + FOREIGN KEY (friend_id) REFERENCES users(user_id) ON DELETE CASCADE, + PRIMARY KEY (user_id, friend_id)); + +CREATE TABLE IF NOT EXISTS mpa ( + mpa_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name VARCHAR NOT NULL); + +CREATE TABLE IF NOT EXISTS films ( + film_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name VARCHAR NOT NULL, + description VARCHAR(200), + release_date DATE, + duration INTEGER, + mpa_id BIGINT, + FOREIGN KEY (mpa_id) REFERENCES mpa(mpa_id) ON DELETE SET NULL); + +CREATE TABLE IF NOT EXISTS genres ( + genre_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name VARCHAR NOT NULL); + +CREATE TABLE IF NOT EXISTS films_genres ( + film_id BIGINT, + genre_id BIGINT, + FOREIGN KEY (film_id) REFERENCES films(film_id) ON DELETE CASCADE, + FOREIGN KEY (genre_id) REFERENCES genres(genre_id) ON DELETE CASCADE, + PRIMARY KEY (film_id, genre_id)); + +CREATE TABLE IF NOT EXISTS likes ( + film_id BIGINT, + user_id BIGINT, + FOREIGN KEY (film_id) REFERENCES films(film_id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, + PRIMARY KEY (film_id, user_id)); \ No newline at end of file From 98d291015b27b66fc313703ab10414b02baeaf02 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:12:09 +0300 Subject: [PATCH 18/26] feat: add tests for db --- .../storage/film/db/DbFilmStorageTest.java | 117 +++++++++++++++++ .../storage/film/db/DbGenreStorageTest.java | 115 +++++++++++++++++ .../storage/film/db/DbLikeStorageTest.java | 37 ++++++ .../storage/film/db/DbMpaStorageTest.java | 52 ++++++++ .../user/db/DbFriendShipStorageTest.java | 59 +++++++++ .../storage/user/db/DbUserStorageTest.java | 119 ++++++++++++++++++ 6 files changed, 499 insertions(+) create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/film/db/DbFilmStorageTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/film/db/DbGenreStorageTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/film/db/DbLikeStorageTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/film/db/DbMpaStorageTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/user/db/DbFriendShipStorageTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/user/db/DbUserStorageTest.java diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/film/db/DbFilmStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/film/db/DbFilmStorageTest.java new file mode 100644 index 0000000..1b79354 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/film/db/DbFilmStorageTest.java @@ -0,0 +1,117 @@ +package ru.yandex.practicum.filmorate.storage.film.db; + +import lombok.RequiredArgsConstructor; +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.dao.EmptyResultDataAccessException; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.storage.mappers.FilmMapper; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@JdbcTest +@Import({DbFilmStorage.class, FilmMapper.class}) +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class DbFilmStorageTest { + + private final DbFilmStorage filmStorage; + + @Test + public void shouldGetFilm() { + Film film = filmStorage.get(1L); + assertNotNull(film); + assertEquals("name1", film.getName()); + } + + @Test + public void shouldThrowWhenNotFound() { + assertThrows(EmptyResultDataAccessException.class, () -> filmStorage.get(1000L)); + } + + @Test + public void shouldGetAll() { + List list = (List) filmStorage.getAll(); + + assertEquals(5, list.size()); + + for (int i = 1; i <= list.size(); i++) { + assertEquals("name" + i, list.get(i - 1).getName()); + } + } + + @Test + public void shouldReturnTrue() { + boolean actual = filmStorage.contains(1L); + assertTrue(actual); + } + + @Test + public void shouldReturnFalse() { + boolean actual = filmStorage.contains(1000L); + assertFalse(actual); + } + + @Test + public void shouldAddFilm() { + Film film = new Film(); + film.setName("name6"); + film.setDescription("desc6"); + + Film actual = filmStorage.add(film); + + assertEquals(6, actual.getId()); + + assertTrue(filmStorage.contains(6L)); + } + + @Test + public void shouldUpdateFilm() { + Film film = filmStorage.get(1L); + film.setName("newName1"); + + Film actual = filmStorage.update(film); + + assertEquals("newName1", actual.getName()); + } + + @Test + public void shouldRemoveFilm() { + filmStorage.remove(1L); + assertFalse(filmStorage.contains(1L)); + } + + @Test + public void shouldGetTopFilmsLimit() { + + Collection topFilms = filmStorage.getTopFilms(2L); + + assertNotNull(topFilms); + assertEquals(2, topFilms.size()); + + List resultList = new ArrayList<>(topFilms); + assertEquals(3L, resultList.get(0).getId()); + assertEquals(2L, resultList.get(1).getId()); + } + + @Test + public void shouldGetTopFilmsNoLimit() { + + Collection topFilms = filmStorage.getTopFilms(1000L); + + assertNotNull(topFilms); + assertEquals(5, topFilms.size()); + + List resultList = new ArrayList<>(topFilms); + assertEquals(3L, resultList.get(0).getId()); + assertEquals(2L, resultList.get(1).getId()); + assertEquals(1L, resultList.get(2).getId()); + } +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/film/db/DbGenreStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/film/db/DbGenreStorageTest.java new file mode 100644 index 0000000..9698668 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/film/db/DbGenreStorageTest.java @@ -0,0 +1,115 @@ +package ru.yandex.practicum.filmorate.storage.film.db; + +import lombok.RequiredArgsConstructor; +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.dao.DataIntegrityViolationException; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.storage.mappers.FilmMapper; +import ru.yandex.practicum.filmorate.storage.mappers.GenreMapper; + +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@JdbcTest +@Import({DbGenreStorage.class, GenreMapper.class, DbFilmStorage.class, FilmMapper.class}) +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class DbGenreStorageTest { + + private final DbGenreStorage storage; + private final DbFilmStorage filmStorage; + + @Test + public void shouldGetGenre() { + + Optional genreOptional = storage.getGenre(1L); + + assertThat(genreOptional) + .isPresent() + .hasValueSatisfying(genre -> + assertThat(genre) + .hasFieldOrPropertyWithValue("id", 1L) + .hasFieldOrPropertyWithValue("name", "Комедия") + ); + } + + @Test + public void shouldGetEmptyGenre() { + Optional genreOptional = storage.getGenre(1000L); + assertThat(genreOptional).isEmpty(); + } + + @Test + public void shouldGetAll() { + List list = (List) storage.getAll(); + assertEquals(6, list.size()); + assertEquals("Боевик", list.getLast().getName()); + } + + @Test + public void shouldAddGenresToFilm() { + Film film = new Film(); + film.setId(1L); + List allGenres = (List) storage.getAll(); + Set genres = new HashSet<>(); + for (int i = 1; i <= 3; i++) { + genres.add(allGenres.get(i - 1)); + } + film.setGenres(genres); + + storage.addGenresToFilm(film); + + Set actual = filmStorage.get(1L).getGenres(); + assertEquals(3, actual.size()); + assertEquals(genres, actual); + } + + @Test + public void shouldThrowIfGenreInvalidAddGenre() { + Film film = new Film(); + film.setId(1L); + Set genres = Set.of(new Genre(1000L, "bla")); + film.setGenres(genres); + + assertThrows(DataIntegrityViolationException.class, () -> storage.addGenresToFilm(film)); + } + + @Test + public void shouldUpdateFilmGenres() { + Film film = new Film(); + film.setId(1L); + List allGenres = (List) storage.getAll(); + Set genres = new HashSet<>(); + for (int i = 1; i <= 3; i++) { + genres.add(allGenres.get(i - 1)); + } + film.setGenres(genres); + + storage.updateFilmGenres(film); + + Set actual = filmStorage.get(1L).getGenres(); + assertEquals(3, actual.size()); + assertEquals(genres, actual); + } + + @Test + public void shouldThrowIfGenreInvalidUpdateGenre() { + Film film = new Film(); + film.setId(1L); + Set genres = Set.of(new Genre(1000L, "bla")); + film.setGenres(genres); + + assertThrows(DataIntegrityViolationException.class, () -> storage.updateFilmGenres(film)); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/film/db/DbLikeStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/film/db/DbLikeStorageTest.java new file mode 100644 index 0000000..5750b1b --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/film/db/DbLikeStorageTest.java @@ -0,0 +1,37 @@ +package ru.yandex.practicum.filmorate.storage.film.db; + +import lombok.RequiredArgsConstructor; +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 static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@JdbcTest +@Import(DbLikeStorage.class) +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class DbLikeStorageTest { + + private final DbLikeStorage storage; + + @Test + public void shouldAddLikes() { + Long likes = storage.addLike(1L, 5L); + assertEquals(3L, likes); + } + + @Test + public void shouldDeleteLikes() { + Long likes = storage.deleteLike(1L, 1L); + assertEquals(1L, likes); + } + + @Test + public void shouldDoNothing() { + assertDoesNotThrow(() -> storage.clearLikes(1L)); + } +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/film/db/DbMpaStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/film/db/DbMpaStorageTest.java new file mode 100644 index 0000000..c02cd38 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/film/db/DbMpaStorageTest.java @@ -0,0 +1,52 @@ +package ru.yandex.practicum.filmorate.storage.film.db; + +import lombok.RequiredArgsConstructor; +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.Mpa; +import ru.yandex.practicum.filmorate.storage.mappers.MpaMapper; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@JdbcTest +@Import({DbMpaStorage.class, MpaMapper.class}) +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class DbMpaStorageTest { + + private final DbMpaStorage storage; + + @Test + public void shouldGetMpa() { + + Optional mpaOptional = storage.getMpa(1L); + + assertThat(mpaOptional) + .isPresent() + .hasValueSatisfying(mpa -> + assertThat(mpa) + .hasFieldOrPropertyWithValue("id", 1L) + .hasFieldOrPropertyWithValue("name", "G") + ); + } + + @Test + public void shouldGetEmptyMpa() { + Optional mpaOptional = storage.getMpa(1000L); + assertThat(mpaOptional).isEmpty(); + } + + @Test + public void shouldGetAll() { + List list = (List) storage.getAll(); + assertEquals(5, list.size()); + assertEquals("NC-17", list.getLast().getName()); + } +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/user/db/DbFriendShipStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/user/db/DbFriendShipStorageTest.java new file mode 100644 index 0000000..2fca837 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/user/db/DbFriendShipStorageTest.java @@ -0,0 +1,59 @@ +package ru.yandex.practicum.filmorate.storage.user.db; + +import lombok.RequiredArgsConstructor; +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 java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +@JdbcTest +@Import({DbFriendShipStorage.class}) +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class DbFriendShipStorageTest { + + private final DbFriendShipStorage storage; + + @Test + public void shouldGetFriend() { + Set friends = storage.getFriends(3L); + assertTrue(friends.contains(1L)); + assertTrue(friends.contains(4L)); + } + + @Test + public void shouldAddFriend() { + storage.addFriend(1L, 4L); + Set list = storage.getFriends(1L); + assertTrue(list.contains(4L)); + } + + @Test + public void shouldDeleteFriend() { + storage.deleteFriend(3L, 1L); + Set list = storage.getFriends(3L); + assertFalse(list.contains(1L)); + } + + @Test + public void shouldGetCommonFriends() { + Set common = storage.getCommonFriends(2L, 3L); + assertTrue(common.contains(1L)); + } + + @Test + public void shouldGetCommonFriendsReverse() { + Set common = storage.getCommonFriends(3L, 2L); + assertTrue(common.contains(1L)); + } + + @Test + public void shouldDoNothing() { + assertDoesNotThrow(() -> storage.deleteUserFromAllFriends(1L)); + } +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/user/db/DbUserStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/user/db/DbUserStorageTest.java new file mode 100644 index 0000000..cf97adf --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/user/db/DbUserStorageTest.java @@ -0,0 +1,119 @@ +package ru.yandex.practicum.filmorate.storage.user.db; + +import lombok.RequiredArgsConstructor; +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.dao.EmptyResultDataAccessException; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.mappers.UserMapper; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@JdbcTest +@Import({DbUserStorage.class, UserMapper.class}) +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class DbUserStorageTest { + + private final DbUserStorage userStorage; + + @Test + public void shouldGetUser() { + User user = userStorage.get(1L); + assertNotNull(user); + assertEquals("name1", user.getName()); + } + + @Test + public void shouldThrowWhenNotFound() { + assertThrows(EmptyResultDataAccessException.class, () -> userStorage.get(1000L)); + } + + @Test + public void shouldGetAll() { + List list = (List) userStorage.getAll(); + + assertEquals(5, list.size()); + + for (int i = 1; i <= list.size(); i++) { + assertEquals("name" + i, list.get(i - 1).getName()); + } + } + + @Test + public void shouldReturnTrue() { + boolean actual = userStorage.contains(1L); + assertTrue(actual); + } + + @Test + public void shouldReturnFalse() { + boolean actual = userStorage.contains(1000L); + assertFalse(actual); + } + + @Test + public void shouldAddUser() { + User user = new User(); + user.setEmail("login6"); + user.setLogin("login6"); + + User actual = userStorage.add(user); + + assertEquals(6, actual.getId()); + + assertTrue(userStorage.contains(6L)); + } + + @Test + public void shouldUpdateUser() { + User user = userStorage.get(1L); + user.setName("newName1"); + + User actual = userStorage.update(user); + + assertEquals("newName1", actual.getName()); + } + + @Test + public void shouldRemoveUser() { + userStorage.remove(1L); + assertFalse(userStorage.contains(1L)); + } + + @Test + public void shouldGetAllFromCollection() { + List ids = List.of(3L, 4L, 5L); + List users = userStorage.getAllFromCollection(ids); + + assertEquals(3, users.size()); + assertEquals("name3", users.get(0).getName()); + assertEquals("name4", users.get(1).getName()); + assertEquals("name5", users.get(2).getName()); + } + + @Test + public void shouldIgnoreIfNotFound() { + List ids = List.of(4000L, 5L, 6L); + assertDoesNotThrow(() -> userStorage.getAllFromCollection(ids)); + } + + @Test + public void shouldIgnoreIfEmpty() { + List ids = Collections.emptyList(); + List list = userStorage.getAllFromCollection(ids); + assertTrue(list.isEmpty()); + } + + @Test + public void shouldReturnEmptyListIfNull() { + List list = userStorage.getAllFromCollection(null); + assertTrue(list.isEmpty()); + } +} From 7a8478a1e7cc441aca3fc2081db6285bfb5226bb Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:12:22 +0300 Subject: [PATCH 19/26] fix: updated test for memory --- .../storage/film/InMemoryFilmStorageTest.java | 50 ++++++++++++++-- .../storage/film/InMemoryLikeStorageTest.java | 29 ++-------- .../user/InMemoryFriendShipStorageTest.java | 58 +++++++++---------- .../storage/user/InMemoryUserStorageTest.java | 18 +++++- 4 files changed, 93 insertions(+), 62 deletions(-) diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorageTest.java index b7fb5b2..e17c170 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorageTest.java @@ -2,9 +2,12 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import ru.yandex.practicum.filmorate.films.film.InMemoryFilmStorage; +import ru.yandex.practicum.filmorate.storage.film.memory.InMemoryFilmStorage; import ru.yandex.practicum.filmorate.model.Film; +import java.time.LocalDate; +import java.util.List; + import static org.junit.jupiter.api.Assertions.*; public class InMemoryFilmStorageTest { @@ -74,8 +77,47 @@ public void shouldReturnFalseIfNotContains() { } @Test - public void shouldReturnSize() { - storage.add(film); - assertEquals(storage.getAll().size(), storage.size()); + public void shouldReturnTopFilms() { + Film film1 = new Film(1L, "Film 1", "Desc 1", LocalDate.now(), 120); + film1.setLikes(10L); + + Film film2 = new Film(2L, "Film 2", "Desc 2", LocalDate.now(), 130); + film2.setLikes(5L); + + Film film3 = new Film(3L, "Film 3", "Desc 3", LocalDate.now(), 140); + film3.setLikes(15L); + + storage.add(film1); + storage.add(film2); + storage.add(film3); + + List top = (List) storage.getTopFilms(5L); + + assertEquals(3,top.size()); + assertEquals(film3, top.get(0)); + assertEquals(film1, top.get(1)); + assertEquals(film2, top.get(2)); + } + + @Test + public void shouldReturnOnlyTwoTopFilms() { + Film film1 = new Film(1L, "Film 1", "Desc 1", LocalDate.now(), 120); + film1.setLikes(10L); + + Film film2 = new Film(2L, "Film 2", "Desc 2", LocalDate.now(), 130); + film2.setLikes(5L); + + Film film3 = new Film(3L, "Film 3", "Desc 3", LocalDate.now(), 140); + film3.setLikes(15L); + + storage.add(film1); + storage.add(film2); + storage.add(film3); + + List top = (List) storage.getTopFilms(2L); + + assertEquals(2,top.size()); + assertEquals(film3, top.get(0)); + assertEquals(film1, top.get(1)); } } diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryLikeStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryLikeStorageTest.java index b51076d..0c9f24c 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryLikeStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryLikeStorageTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import ru.yandex.practicum.filmorate.storage.film.memory.InMemoryLikeStorage; import java.lang.reflect.Field; import java.util.Map; @@ -29,23 +30,9 @@ private Map> getLikesField() { } } - @Test - public void shouldCreateLikesSet() { - storage.initializeLikesSet(1L); - assertNotNull(getLikesField().get(1L)); - } - - @Test - public void shouldRemoveSet() { - storage.initializeLikesSet(1L); - storage.clearLikesSet(1L); - assertNull(getLikesField().get(1L)); - } @Test public void shouldAddLike() { - storage.initializeLikesSet(1L); - storage.addLike(1L, 1L); storage.addLike(1L, 2L); @@ -54,7 +41,6 @@ public void shouldAddLike() { @Test public void shouldAddLikeOnlyOnce() { - storage.initializeLikesSet(1L); storage.addLike(1L, 1L); storage.addLike(1L, 1L); @@ -62,14 +48,8 @@ public void shouldAddLikeOnlyOnce() { assertEquals(1, getLikesField().get(1L).size()); } - @Test - public void shouldThrowNullPointerExceptionWhenAddLikeToFilmThatDoesNotExist() { - assertThrows(NullPointerException.class, () -> storage.addLike(1L, 1L)); - } - @Test public void shouldRemoveLike() { - storage.initializeLikesSet(1L); storage.addLike(1L, 1L); storage.addLike(1L, 2L); @@ -81,7 +61,6 @@ public void shouldRemoveLike() { @Test public void shouldRemoveLikeOnlyOnce() { - storage.initializeLikesSet(1L); storage.addLike(1L, 1L); storage.addLike(1L, 2L); @@ -93,7 +72,9 @@ public void shouldRemoveLikeOnlyOnce() { } @Test - public void shouldThrowNullPointerExceptionWhenRemoveLikeToFilmThatDoesNotExist() { - assertThrows(NullPointerException.class, () -> storage.deleteLike(1L, 1L)); + public void shouldClearLikes() { + storage.addLike(1L, 2L); + storage.clearLikes(1L); + assertFalse(getLikesField().containsKey(1L)); } } diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryFriendShipStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryFriendShipStorageTest.java index 058ac5b..802db3a 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryFriendShipStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryFriendShipStorageTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import ru.yandex.practicum.filmorate.storage.user.memory.InMemoryFriendShipStorage; import java.lang.reflect.Field; import java.util.Map; @@ -32,8 +33,6 @@ private Map> getFriendShips() { @Test public void shouldCreateFriendShip() { - friendShipStorage.initializeFriendsSet(1L); - friendShipStorage.initializeFriendsSet(2L); friendShipStorage.addFriend(1L, 2L); friendShipStorage.addFriend(2L, 1L); @@ -42,27 +41,15 @@ public void shouldCreateFriendShip() { assertTrue(getFriendShips().get(2L).contains(1L)); } - @Test - public void shouldThrowNullPointerExceptionWhenSenderIsNotFoundCreateFriendShip() { - assertThrows(NullPointerException.class, () -> friendShipStorage.addFriend(1000L, 1L)); - } - - @Test - public void shouldThrowNullPointerExceptionWhenReceiverIsNotFoundCreateFriendShip() { - assertThrows(NullPointerException.class, () -> friendShipStorage.addFriend(1L, 1000L)); - } - @Test public void shouldClearUserFriendsAndDeleteUser() { - friendShipStorage.initializeFriendsSet(1L); - friendShipStorage.clearFriendsSet(1L); + friendShipStorage.addFriend(1L, 2L); + friendShipStorage.deleteUserFromAllFriends(1L); assertNull(getFriendShips().get(1L)); } @Test public void shouldBreakFriendShip() { - friendShipStorage.initializeFriendsSet(1L); - friendShipStorage.initializeFriendsSet(2L); friendShipStorage.addFriend(1L, 2L); friendShipStorage.addFriend(2L, 1L); @@ -74,20 +61,8 @@ public void shouldBreakFriendShip() { assertFalse(getFriendShips().get(2L).contains(1L)); } - @Test - public void shouldThrowNullPointerExceptionWhenSenderIsNotFoundBreakFriendShip() { - assertThrows(NullPointerException.class, () -> friendShipStorage.deleteFriend(1000L, 1L)); - } - - @Test - public void shouldThrowNullPointerExceptionWhenReceiverIsNotFoundBreakFriendShip() { - assertThrows(NullPointerException.class, () -> friendShipStorage.deleteFriend(1L, 1000L)); - } - @Test public void shouldReturnFriends() { - friendShipStorage.initializeFriendsSet(1L); - friendShipStorage.initializeFriendsSet(2L); friendShipStorage.addFriend(1L, 2L); friendShipStorage.addFriend(2L, 1L); @@ -96,7 +71,28 @@ public void shouldReturnFriends() { } @Test - public void shouldThrowNullPointerExceptionWhenUserIsNotFoundReturnFriends() { - assertNull(friendShipStorage.getFriends(1000L)); + public void shouldGetCommonFriends() { + friendShipStorage.addFriend(1L, 2L); + friendShipStorage.addFriend(1L, 3L); + friendShipStorage.addFriend(2L, 3L); + + Set common = friendShipStorage.getCommonFriends(1L, 2L); + + assertTrue(common.contains(3L)); + assertFalse(common.contains(1L)); + assertFalse(common.contains(2L)); + } + + @Test + public void shouldGetCommonFriendsReverseOrder() { + friendShipStorage.addFriend(1L, 2L); + friendShipStorage.addFriend(1L, 3L); + friendShipStorage.addFriend(2L, 3L); + + Set common = friendShipStorage.getCommonFriends(2L, 1L); + + assertTrue(common.contains(3L)); + assertFalse(common.contains(1L)); + assertFalse(common.contains(2L)); } -} \ No newline at end of file +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorageTest.java index cd48557..e8ef364 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorageTest.java @@ -3,8 +3,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.user.memory.InMemoryUserStorage; import java.lang.reflect.Field; +import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; @@ -89,8 +91,18 @@ public void shouldReturnFalseIfNotContains() { } @Test - public void shouldReturnSize() { - storage.add(user); - assertEquals(storage.getAll().size(), storage.size()); + public void shouldGetAllFromCollection() { + List ids = List.of(1L, 2L, 3L); + for (int i = 0; i < 3; i++) { + User user = new User(); + user.setName(i + ""); + storage.add(user); + } + + List users = storage.getAllFromCollection(ids); + + assertEquals("0", users.get(0).getName()); + assertEquals("1", users.get(1).getName()); + assertEquals("2", users.get(2).getName()); } } From 09784763f7992371806d9fd589eba030b261a910 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:12:36 +0300 Subject: [PATCH 20/26] fix: updated tests --- .../service/film/FilmServiceTest.java | 131 +++++++++++++++--- .../service/user/UserServiceTest.java | 60 ++------ 2 files changed, 120 insertions(+), 71 deletions(-) diff --git a/src/test/java/ru/yandex/practicum/filmorate/service/film/FilmServiceTest.java b/src/test/java/ru/yandex/practicum/filmorate/service/film/FilmServiceTest.java index d883d1a..9c81fa8 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/service/film/FilmServiceTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/service/film/FilmServiceTest.java @@ -8,14 +8,14 @@ import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Genre; import ru.yandex.practicum.filmorate.storage.film.FilmStorage; +import ru.yandex.practicum.filmorate.storage.film.GenreStorage; import ru.yandex.practicum.filmorate.storage.film.LikeStorage; import ru.yandex.practicum.filmorate.storage.user.UserStorage; import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; +import java.util.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -32,11 +32,14 @@ public class FilmServiceTest { @Mock private UserStorage userStorage; + @Mock + private GenreStorage genreStorage; + private FilmService filmService; @BeforeEach public void setUp() { - filmService = new FilmService(filmStorage, userStorage, likeStorage); + filmService = new FilmService(filmStorage, userStorage, likeStorage, genreStorage); } @Test @@ -87,14 +90,64 @@ public void shouldAddFilmSuccessfully() { Film savedFilm = new Film(1L, "New Film", "Description", LocalDate.now(), 120); when(filmStorage.add(inputFilm)).thenReturn(savedFilm); - doNothing().when(likeStorage).initializeLikesSet(1L); Film result = filmService.addFilm(inputFilm); assertNotNull(result); assertEquals(1L, result.getId()); verify(filmStorage).add(inputFilm); - verify(likeStorage).initializeLikesSet(1L); + } + + @Test + public void shouldAddFilmWithGenres() { + Film inputFilm = new Film(null, "New Film", "Description", LocalDate.now(), 120); + inputFilm.setGenres(Set.of(new Genre(1L, "Комедия"), new Genre(2L, "Драма"))); + + Film savedFilm = new Film(1L, "New Film", "Description", LocalDate.now(), 120); + savedFilm.setGenres(inputFilm.getGenres()); + + when(filmStorage.add(inputFilm)).thenReturn(savedFilm); + + Film result = filmService.addFilm(inputFilm); + + assertNotNull(result); + assertEquals(1L, result.getId()); + verify(filmStorage).add(inputFilm); + verify(genreStorage).addGenresToFilm(savedFilm); + } + + @Test + public void shouldAddFilmWithoutGenres() { + Film inputFilm = new Film(null, "New Film", "Description", LocalDate.now(), 120); + inputFilm.setGenres(null); + + Film savedFilm = new Film(1L, "New Film", "Description", LocalDate.now(), 120); + + when(filmStorage.add(inputFilm)).thenReturn(savedFilm); + + Film result = filmService.addFilm(inputFilm); + + assertNotNull(result); + assertEquals(1L, result.getId()); + verify(filmStorage).add(inputFilm); + verify(genreStorage, never()).addGenresToFilm(any()); + } + + @Test + public void shouldAddFilmWithEmptyGenres() { + Film inputFilm = new Film(null, "New Film", "Description", LocalDate.now(), 120); + inputFilm.setGenres(Collections.emptySet()); + + Film savedFilm = new Film(1L, "New Film", "Description", LocalDate.now(), 120); + + when(filmStorage.add(inputFilm)).thenReturn(savedFilm); + + Film result = filmService.addFilm(inputFilm); + + assertNotNull(result); + assertEquals(1L, result.getId()); + verify(filmStorage).add(inputFilm); + verify(genreStorage, never()).addGenresToFilm(any()); } @Test @@ -132,6 +185,43 @@ public void shouldThrowNotFoundExceptionWhenUpdateNonExistentFilm() { verify(filmStorage, never()).update(any()); } + @Test + public void shouldUpdateFilmWithGenres() { + Film inputFilm = new Film(1L, "Updated Film", "Updated Description", LocalDate.now(), 150); + inputFilm.setGenres(Set.of(new Genre(1L, "Комедия"), new Genre(3L, "Мультфильм"))); + + Film updatedFilm = new Film(1L, "Updated Film", "Updated Description", LocalDate.now(), 150); + updatedFilm.setGenres(inputFilm.getGenres()); + + when(filmStorage.contains(1L)).thenReturn(true); + when(filmStorage.update(inputFilm)).thenReturn(updatedFilm); + + Film result = filmService.updateFilm(inputFilm); + + assertNotNull(result); + assertEquals(1L, result.getId()); + verify(filmStorage).update(inputFilm); + verify(genreStorage).updateFilmGenres(updatedFilm); + } + + @Test + public void shouldUpdateFilmWithoutGenres() { + Film inputFilm = new Film(1L, "Updated Film", "Updated Description", LocalDate.now(), 150); + inputFilm.setGenres(null); + + Film updatedFilm = new Film(1L, "Updated Film", "Updated Description", LocalDate.now(), 150); + + when(filmStorage.contains(1L)).thenReturn(true); + when(filmStorage.update(inputFilm)).thenReturn(updatedFilm); + + Film result = filmService.updateFilm(inputFilm); + + assertNotNull(result); + assertEquals(1L, result.getId()); + verify(filmStorage).update(inputFilm); + verify(genreStorage, never()).updateFilmGenres(any()); + } + @Test public void shouldDeleteFilmSuccessfully() { Long filmId = 1L; @@ -141,7 +231,7 @@ public void shouldDeleteFilmSuccessfully() { filmService.deleteFilm(filmId); verify(filmStorage).remove(filmId); - verify(likeStorage).clearLikesSet(filmId); + verify(likeStorage).clearLikes(filmId); } @Test @@ -152,7 +242,7 @@ public void shouldThrowNotFoundExceptionWhenDeleteNonExistentFilm() { assertThrows(NotFoundException.class, () -> filmService.deleteFilm(filmId)); verify(filmStorage, never()).remove(filmId); - verify(likeStorage, never()).clearLikesSet(filmId); + verify(likeStorage, never()).clearLikes(filmId); } @Test @@ -171,7 +261,7 @@ public void shouldAddLikeSuccessfully() { assertNotNull(result); assertEquals(5L, result.getLikes()); verify(likeStorage).addLike(filmId, userId); - verify(filmStorage, times(2)).get(filmId); + verify(filmStorage).get(filmId); } @Test @@ -247,7 +337,7 @@ public void shouldGetTopFilmsOrderedByLikes() { Film film3 = new Film(3L, "Film 3", "Desc 3", LocalDate.now(), 130); film3.setLikes(5L); - when(filmStorage.getAll()).thenReturn(List.of(film1, film2, film3)); + when(filmStorage.getTopFilms(2L)).thenReturn(List.of(film2, film1)); Collection topFilms = filmService.getTopFilms(2L); @@ -257,13 +347,11 @@ public void shouldGetTopFilmsOrderedByLikes() { List resultList = new ArrayList<>(topFilms); assertEquals(25L, resultList.get(0).getLikes()); // film2 assertEquals(10L, resultList.get(1).getLikes()); // film1 + verify(filmStorage).getTopFilms(2L); } @Test public void shouldReturnEmptyListWhenNoFilms() { - when(filmStorage.getAll()).thenReturn(List.of()); - - // when Collection topFilms = filmService.getTopFilms(10L); assertNotNull(topFilms); @@ -272,17 +360,18 @@ public void shouldReturnEmptyListWhenNoFilms() { @Test public void shouldReturnAllFilmsWhenCountIsGreaterThanTotal() { - Film film1 = new Film(1L, "Film 1", "Desc 1", LocalDate.now(), 120); - film1.setLikes(5L); - Film film2 = new Film(2L, "Film 2", "Desc 2", LocalDate.now(), 150); - film2.setLikes(3L); + List allFilms = List.of( + new Film(1L, "Film 1", "Desc 1", LocalDate.now(), 120), + new Film(2L, "Film 2", "Desc 2", LocalDate.now(), 150), + new Film(3L, "Film 3", "Desc 3", LocalDate.now(), 130) + ); - when(filmStorage.getAll()).thenReturn(List.of(film1, film2)); + when(filmStorage.getTopFilms(10L)).thenReturn(allFilms); - // when - Collection topFilms = filmService.getTopFilms(5L); + Collection topFilms = filmService.getTopFilms(10L); assertNotNull(topFilms); - assertEquals(2, topFilms.size()); + assertEquals(3, topFilms.size()); + verify(filmStorage).getTopFilms(10L); } } \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/service/user/UserServiceTest.java b/src/test/java/ru/yandex/practicum/filmorate/service/user/UserServiceTest.java index a314020..9b22297 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/service/user/UserServiceTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/service/user/UserServiceTest.java @@ -13,7 +13,6 @@ import java.time.LocalDate; import java.util.Collection; -import java.util.HashSet; import java.util.List; import java.util.Set; @@ -78,13 +77,11 @@ public void shouldAddUser() { User returnUser = new User(1L, "email@mail.com", "login", "name", LocalDate.now()); when(userStorage.add(any(User.class))).thenReturn(returnUser); - doNothing().when(friendShipStorage).initializeFriendsSet(1L); User result = userService.addUser(user1); assertEquals(returnUser, result); verify(userStorage).add(any(User.class)); - verify(friendShipStorage).initializeFriendsSet(1L); } @Test @@ -189,19 +186,12 @@ public void shouldThrowNotFoundWhenUpdateUserWhenIdNotFound() { @Test public void shouldDeleteUser() { Long userId = 1L; - User user = new User(userId, "email@mail.com", "login", "name", LocalDate.now()); when(userStorage.contains(userId)).thenReturn(true); - when(friendShipStorage.getFriends(userId)).thenReturn(new HashSet<>(Set.of(2L, 3L))); - when(friendShipStorage.getFriends(2L)).thenReturn(new HashSet<>(Set.of(1L, 4L))); - when(friendShipStorage.getFriends(3L)).thenReturn(new HashSet<>(Set.of(1L, 5L))); - userService.deleteUser(userId); verify(userStorage).remove(userId); - verify(friendShipStorage).clearFriendsSet(userId); - verify(friendShipStorage).getFriends(2L); - verify(friendShipStorage).getFriends(3L); + verify(friendShipStorage).deleteUserFromAllFriends(1L); } @Test @@ -210,27 +200,6 @@ public void shouldThrowNotFoundWhenDeleteUserWhenIdNotFound() { assertThrows(NotFoundException.class, () -> userService.deleteUser(1000L)); verify(userStorage, never()).remove(1000L); - verify(friendShipStorage, never()).clearFriendsSet(1000L); - } - - @Test - public void shouldRemoveUserFromFriendsFriendsLists() { - Long userId = 1L; - Set friends = Set.of(2L, 3L); - - Set friend2Friends = new HashSet<>(Set.of(1L, 4L, 5L)); - Set friend3Friends = new HashSet<>(Set.of(1L, 6L)); - - when(userStorage.contains(userId)).thenReturn(true); - when(friendShipStorage.getFriends(userId)).thenReturn(friends); - when(friendShipStorage.getFriends(2L)).thenReturn(friend2Friends); - when(friendShipStorage.getFriends(3L)).thenReturn(friend3Friends); - - userService.deleteUser(userId); - - assertFalse(friend2Friends.contains(userId)); - assertFalse(friend3Friends.contains(userId)); - verify(userStorage).remove(userId); } @Test @@ -244,7 +213,7 @@ public void shouldAddFriendSuccessfully() { userService.addFriend(senderId, receiverId); verify(friendShipStorage).addFriend(senderId, receiverId); - verify(friendShipStorage).addFriend(receiverId, senderId); + verify(friendShipStorage, never()).addFriend(receiverId, senderId); } @Test @@ -292,7 +261,7 @@ public void shouldDeleteFriendSuccessfully() { userService.deleteFriend(senderId, receiverId); verify(friendShipStorage).deleteFriend(senderId, receiverId); - verify(friendShipStorage).deleteFriend(receiverId, senderId); + verify(friendShipStorage, never()).deleteFriend(receiverId, senderId); } @Test @@ -327,9 +296,7 @@ public void shouldGetFriendsSuccessfully() { when(userStorage.contains(userId)).thenReturn(true); when(friendShipStorage.getFriends(userId)).thenReturn(friendIds); - when(userStorage.get(2L)).thenReturn(friend1); - when(userStorage.get(3L)).thenReturn(friend2); - + when(userStorage.getAllFromCollection(friendIds)).thenReturn(List.of(friend1, friend2)); Collection friends = userService.getFriends(userId); assertNotNull(friends); @@ -337,8 +304,7 @@ public void shouldGetFriendsSuccessfully() { assertTrue(friends.contains(friend1)); assertTrue(friends.contains(friend2)); verify(friendShipStorage).getFriends(userId); - verify(userStorage).get(2L); - verify(userStorage).get(3L); + verify(userStorage).getAllFromCollection(friendIds); } @Test @@ -368,19 +334,16 @@ public void shouldThrowNotFoundExceptionWhenGettingFriendsOfNonExistentUser() { public void shouldGetCommonFriendsSuccessfully() { Long user1Id = 1L; Long user2Id = 2L; - Set user1Friends = Set.of(3L, 4L, 5L); - Set user2Friends = Set.of(4L, 5L, 6L); + Set common = Set.of(4L, 5L); User commonFriend1 = new User(4L, "common1@mail.com", "common1", "Common One", LocalDate.now()); User commonFriend2 = new User(5L, "common2@mail.com", "common2", "Common Two", LocalDate.now()); + List commonUsers = List.of(commonFriend1, commonFriend2); when(userStorage.contains(user1Id)).thenReturn(true); when(userStorage.contains(user2Id)).thenReturn(true); - when(friendShipStorage.getFriends(user1Id)).thenReturn(user1Friends); - when(friendShipStorage.getFriends(user2Id)).thenReturn(user2Friends); - when(userStorage.get(4L)).thenReturn(commonFriend1); - when(userStorage.get(5L)).thenReturn(commonFriend2); - + when(friendShipStorage.getCommonFriends(user1Id, user2Id)).thenReturn(common); + when(userStorage.getAllFromCollection(common)).thenReturn(commonUsers); Collection commonFriends = userService.getCommonFriends(user1Id, user2Id); assertNotNull(commonFriends); @@ -393,13 +356,10 @@ public void shouldGetCommonFriendsSuccessfully() { public void shouldReturnEmptyCommonFriendsWhenNoCommonFriends() { Long user1Id = 1L; Long user2Id = 2L; - Set user1Friends = Set.of(3L, 4L); - Set user2Friends = Set.of(5L, 6L); when(userStorage.contains(user1Id)).thenReturn(true); when(userStorage.contains(user2Id)).thenReturn(true); - when(friendShipStorage.getFriends(user1Id)).thenReturn(user1Friends); - when(friendShipStorage.getFriends(user2Id)).thenReturn(user2Friends); + when(friendShipStorage.getCommonFriends(user1Id, user2Id)).thenReturn(Set.of()); Collection commonFriends = userService.getCommonFriends(user1Id, user2Id); From 04016c1d6d5dd6b02636332efc1a118c33b8df5f Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:12:49 +0300 Subject: [PATCH 21/26] feat: add test for new services --- .../service/film/GenreServiceTest.java | 71 +++++++++++++++++++ .../service/film/MpaServiceTest.java | 71 +++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 src/test/java/ru/yandex/practicum/filmorate/service/film/GenreServiceTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/service/film/MpaServiceTest.java diff --git a/src/test/java/ru/yandex/practicum/filmorate/service/film/GenreServiceTest.java b/src/test/java/ru/yandex/practicum/filmorate/service/film/GenreServiceTest.java new file mode 100644 index 0000000..601fbd0 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/service/film/GenreServiceTest.java @@ -0,0 +1,71 @@ +package ru.yandex.practicum.filmorate.service.film; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.storage.film.GenreStorage; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class GenreServiceTest { + + @Mock + private GenreStorage genreStorage; + + private GenreService mpaService; + + @BeforeEach + public void setUp() { + mpaService = new GenreService(genreStorage); + } + + @Test + public void shouldGetGenre() { + Genre mpa = new Genre(1L, "name1"); + when(genreStorage.getGenre(1L)).thenReturn(Optional.of(mpa)); + + Genre actualGenre = mpaService.getGenre(1L); + + assertNotNull(actualGenre); + assertEquals(actualGenre, mpa); + verify(genreStorage).getGenre(1L); + } + + @Test + public void shouldThrowNotFoundWhenNull() { + when(genreStorage.getGenre(1L)).thenReturn(Optional.ofNullable(null)); + + assertThrows(NotFoundException.class, () -> mpaService.getGenre(1L)); + + verify(genreStorage).getGenre(1L); + } + + @Test + public void shouldThrowNotFound() { + when(genreStorage.getGenre(1L)).thenReturn(Optional.empty()); + + assertThrows(NotFoundException.class, () -> mpaService.getGenre(1L)); + + verify(genreStorage).getGenre(1L); + } + + @Test + public void shouldGetAll() { + when(genreStorage.getAll()).thenReturn(List.of(new Genre(1L, "name1"), new Genre(2L, "name2"))); + + List list = (List) mpaService.getAll(); + + assertEquals(2, list.size()); + verify(genreStorage).getAll(); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/service/film/MpaServiceTest.java b/src/test/java/ru/yandex/practicum/filmorate/service/film/MpaServiceTest.java new file mode 100644 index 0000000..5e17a2a --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/service/film/MpaServiceTest.java @@ -0,0 +1,71 @@ +package ru.yandex.practicum.filmorate.service.film; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Mpa; +import ru.yandex.practicum.filmorate.storage.film.MpaStorage; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class MpaServiceTest { + + @Mock + private MpaStorage mpaStorage; + + private MpaService mpaService; + + @BeforeEach + public void setUp() { + mpaService = new MpaService(mpaStorage); + } + + @Test + public void shouldGetMpa() { + Mpa mpa = new Mpa(1L, "name1"); + when(mpaStorage.getMpa(1L)).thenReturn(Optional.of(mpa)); + + Mpa actualMpa = mpaService.getMpa(1L); + + assertNotNull(actualMpa); + assertEquals(actualMpa, mpa); + verify(mpaStorage).getMpa(1L); + } + + @Test + public void shouldThrowNotFoundWhenNull() { + when(mpaStorage.getMpa(1L)).thenReturn(Optional.ofNullable(null)); + + assertThrows(NotFoundException.class, () -> mpaService.getMpa(1L)); + + verify(mpaStorage).getMpa(1L); + } + + @Test + public void shouldThrowNotFound() { + when(mpaStorage.getMpa(1L)).thenReturn(Optional.empty()); + + assertThrows(NotFoundException.class, () -> mpaService.getMpa(1L)); + + verify(mpaStorage).getMpa(1L); + } + + @Test + public void shouldGetAll() { + when(mpaStorage.getAll()).thenReturn(List.of(new Mpa(1L, "name1"), new Mpa(2L, "name2"))); + + List list = (List) mpaService.getAll(); + + assertEquals(2, list.size()); + verify(mpaStorage).getAll(); + } +} From 9ae5d7f359430c7ef2c9ba3497e56d4cceb43807 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:12:56 +0300 Subject: [PATCH 22/26] feat: add test for new controllers --- .../controller/GenreControllerTest.java | 77 +++++++++++++++++++ .../controller/MpaControllerTest.java | 77 +++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 src/test/java/ru/yandex/practicum/filmorate/controller/GenreControllerTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/controller/MpaControllerTest.java diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/GenreControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/GenreControllerTest.java new file mode 100644 index 0000000..8497549 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/GenreControllerTest.java @@ -0,0 +1,77 @@ +package ru.yandex.practicum.filmorate.controller; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.service.film.GenreService; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(GenreController.class) +public class GenreControllerTest extends ControllerTest { + + @MockBean + GenreService genreService; + + @Test + public void shouldGetGenre() throws Exception { + when(genreService.getGenre(1L)) + .thenReturn(new Genre(1L, "name")); + + mockMvc.perform(get("/genres/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("name")); + verify(genreService).getGenre(1L); + } + + @Test + public void shouldReturn404WhenUserNotFoundAndGetGenre() throws Exception { + when(genreService.getGenre(1000L)) + .thenThrow(new NotFoundException("Not Found")); + mockMvc.perform(get("/genres/1000")).andExpect(status().isNotFound()); + verify(genreService).getGenre(any()); + } + + @Test + public void shouldReturn400WhenNegativeIdsAndGetGenre() throws Exception { + mockMvc.perform(get("/genres/-1")).andExpect(status().isBadRequest()); + verify(genreService, never()).getGenre(any()); + } + + @Test + public void shouldReturn400WhenZeroUserIdAndGetGenre() throws Exception { + mockMvc.perform(get("/genres/0")).andExpect(status().isBadRequest()); + verify(genreService, never()).getGenre(any()); + } + + @Test + public void shouldGetGenres() throws Exception { + Genre mpa1 = new Genre(1L, "name1"); + Genre mpa2 = new Genre(2L, "name2"); + + List genres = List.of(mpa1, mpa2); + + // Настраиваем мок сервиса + when(genreService.getAll()).thenReturn(genres); + + mockMvc.perform(get("/genres")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$.length()").value(2)) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[0].name").value("name1")) + .andExpect(jsonPath("$[1].id").value(2)) + .andExpect(jsonPath("$[1].name").value("name2")); + + verify(genreService, times(1)).getAll(); + } +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/MpaControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/MpaControllerTest.java new file mode 100644 index 0000000..f728905 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/MpaControllerTest.java @@ -0,0 +1,77 @@ +package ru.yandex.practicum.filmorate.controller; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Mpa; +import ru.yandex.practicum.filmorate.service.film.MpaService; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(MpaController.class) +public class MpaControllerTest extends ControllerTest { + + @MockBean + MpaService mpaService; + + @Test + public void shouldGetMpa() throws Exception { + when(mpaService.getMpa(1L)) + .thenReturn(new Mpa(1L, "name")); + + mockMvc.perform(get("/mpa/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("name")); + verify(mpaService).getMpa(1L); + } + + @Test + public void shouldReturn404WhenUserNotFoundAndGetMpa() throws Exception { + when(mpaService.getMpa(1000L)) + .thenThrow(new NotFoundException("Not Found")); + mockMvc.perform(get("/mpa/1000")).andExpect(status().isNotFound()); + verify(mpaService).getMpa(any()); + } + + @Test + public void shouldReturn400WhenNegativeIdsAndGetMpa() throws Exception { + mockMvc.perform(get("/mpa/-1")).andExpect(status().isBadRequest()); + verify(mpaService, never()).getMpa(any()); + } + + @Test + public void shouldReturn400WhenZeroUserIdAndGetMpa() throws Exception { + mockMvc.perform(get("/mpa/0")).andExpect(status().isBadRequest()); + verify(mpaService, never()).getMpa(any()); + } + + @Test + public void shouldGetMpas() throws Exception { + Mpa mpa1 = new Mpa(1L, "name1"); + Mpa mpa2 = new Mpa(2L, "name2"); + + List mpas = List.of(mpa1, mpa2); + + // Настраиваем мок сервиса + when(mpaService.getAll()).thenReturn(mpas); + + mockMvc.perform(get("/mpa")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$.length()").value(2)) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[0].name").value("name1")) + .andExpect(jsonPath("$[1].id").value(2)) + .andExpect(jsonPath("$[1].name").value("name2")); + + verify(mpaService, times(1)).getAll(); + } +} From 41136efb3382205139cce06bb83b17b97da2d7e8 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:13:09 +0300 Subject: [PATCH 23/26] fix: updated tests for controllers --- .../controller/FilmControllerTest.java | 118 +++++++++++++++++- .../controller/UserControllerTest.java | 57 +++++++++ 2 files changed, 173 insertions(+), 2 deletions(-) diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java index ce4fdbf..c674d91 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java @@ -4,14 +4,19 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.MediaType; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Mpa; import ru.yandex.practicum.filmorate.service.film.FilmService; import java.time.LocalDate; import java.util.Collections; import java.util.List; +import java.util.Set; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; @@ -141,6 +146,64 @@ public void shouldReturn400WhenZeroCount() throws Exception { verify(filmService, never()).getTopFilms(any()); } + + + @Test + public void shouldGetFilm() throws Exception { + when(filmService.getFilm(1L)) + .thenReturn(new Film(1L, "name", "description", LocalDate.MAX, 100)); + + mockMvc.perform(get("/films/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("name")); + verify(filmService).getFilm(1L); + } + + @Test + public void shouldReturn404WhenFilmNotFoundAndGetFilm() throws Exception { + when(filmService.getFilm(1000L)) + .thenThrow(new NotFoundException("Not Found")); + mockMvc.perform(get("/films/1000")).andExpect(status().isNotFound()); + verify(filmService).getFilm(any()); + } + + @Test + public void shouldReturn400WhenNegativeIdsAndGetFilm() throws Exception { + mockMvc.perform(get("/films/-1")).andExpect(status().isBadRequest()); + verify(filmService, never()).getFilm(any()); + } + + @Test + public void shouldReturn400WhenZeroFilmIdAndGetFilm() throws Exception { + mockMvc.perform(get("/films/0")).andExpect(status().isBadRequest()); + verify(filmService, never()).getFilm(any()); + } + + @Test + public void shouldDeleteFilm() throws Exception { + mockMvc.perform(delete("/films/1")).andExpect(status().isOk()); + verify(filmService).deleteFilm(any()); + } + + @Test + public void shouldReturn404WhenFilmNotFoundAndDeleteFilm() throws Exception { + doThrow(new NotFoundException("Not Found")).when(filmService).deleteFilm(1000L); + mockMvc.perform(delete("/films/1000")).andExpect(status().isNotFound()); + verify(filmService).deleteFilm(any()); + } + + @Test + public void shouldReturn400WhenNegativeIdsAndDeleteFilm() throws Exception { + mockMvc.perform(delete("/films/-1")).andExpect(status().isBadRequest()); + verify(filmService, never()).deleteFilm(any()); + } + + @Test + public void shouldReturn400WhenZeroFilmIdAndDeleteFilm() throws Exception { + mockMvc.perform(delete("/films/0")).andExpect(status().isBadRequest()); + verify(filmService, never()).deleteFilm(any()); + } } @Nested @@ -283,6 +346,57 @@ public void shouldReturn400WhenInvalidDateFormat() throws Exception { verify(filmService, never()).addFilm(any()); } + + @Test + public void shouldNotCreateFilmWhenMpaIsNotFoundAndReturnNotFound() throws Exception { + Film filmToCreate = new Film(null, "name", "description", + LocalDate.of(2000, 1, 1), 10); + filmToCreate.setMpa(new Mpa(1000L, "name")); + + when(filmService.addFilm(filmToCreate)).thenThrow(new DataIntegrityViolationException("foreign key mpa")); + String invalidJson = objectMapper.writeValueAsString(filmToCreate); + + + mockMvc.perform(post("/films") + .contentType(MediaType.APPLICATION_JSON) + .content(invalidJson)) + .andExpect(status().isNotFound()); + + verify(filmService).addFilm(any()); + } + + @Test + public void shouldNotCreateFilmWhenGenreIsNotFoundAndReturnNotFound() throws Exception { + Film filmToCreate = new Film(null, "name", "description", + LocalDate.of(2000, 1, 1), 10); + filmToCreate.setGenres(Set.of(new Genre(1000L, "bla"))); + + when(filmService.addFilm(filmToCreate)).thenThrow(new DataIntegrityViolationException("foreign key genre")); + String invalidJson = objectMapper.writeValueAsString(filmToCreate); + + mockMvc.perform(post("/films") + .contentType(MediaType.APPLICATION_JSON) + .content(invalidJson)) + .andExpect(status().isNotFound()); + + verify(filmService).addFilm(any()); + } + + @Test + public void shouldNotCreateFilmWhenDataBrokenAndReturnBadRequest() throws Exception { + Film filmToCreate = new Film(null, "name", "description", + LocalDate.of(2000, 1, 1), 10); + + when(filmService.addFilm(filmToCreate)).thenThrow(new DataIntegrityViolationException("something")); + String invalidJson = objectMapper.writeValueAsString(filmToCreate); + + mockMvc.perform(post("/films") + .contentType(MediaType.APPLICATION_JSON) + .content(invalidJson)) + .andExpect(status().isBadRequest()); + + verify(filmService).addFilm(any()); + } } @Nested @@ -379,7 +493,7 @@ public void shouldAddLike() throws Exception { } @Test - public void shouldNotAddLikeWhenFilmNotFound() throws Exception { + public void shouldNotAddLikeWhenUserNotFound() throws Exception { Film filmToUpdate = new Film(1000L, "ONLY TODAY", "NEW TEXT", LocalDate.of(2000, 1, 1), 120); @@ -397,7 +511,7 @@ public void shouldNotAddLikeWhenFilmNotFound() throws Exception { } @Test - public void shouldNotAddLikeWhenUserNotFound() throws Exception { + public void shouldNotAddLikeWhenFilmNotFound() throws Exception { Film filmToUpdate = new Film(1L, "ONLY TODAY", "NEW TEXT", LocalDate.of(2000, 1, 1), 120); diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java index 7884b1a..540a5f9 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java @@ -172,6 +172,63 @@ public void shouldReturn400WhenZeroUserId() throws Exception { verify(userService, never()).getFriends(any()); } + + @Test + public void shouldGetUser() throws Exception { + when(userService.getUser(1L)) + .thenReturn(new User(1L, "email", "login", "name", LocalDate.MIN)); + + mockMvc.perform(get("/users/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.email").value("email")); + verify(userService).getUser(1L); + } + + @Test + public void shouldReturn404WhenUserNotFoundAndGetUser() throws Exception { + when(userService.getUser(1000L)) + .thenThrow(new NotFoundException("Not Found")); + mockMvc.perform(get("/users/1000")).andExpect(status().isNotFound()); + verify(userService).getUser(any()); + } + + @Test + public void shouldReturn400WhenNegativeIdsAndGetUser() throws Exception { + mockMvc.perform(get("/users/-1")).andExpect(status().isBadRequest()); + verify(userService, never()).getUser(any()); + } + + @Test + public void shouldReturn400WhenZeroUserIdAndGetUser() throws Exception { + mockMvc.perform(get("/users/0")).andExpect(status().isBadRequest()); + verify(userService, never()).getUser(any()); + } + + @Test + public void shouldDeleteUser() throws Exception { + mockMvc.perform(delete("/users/1")).andExpect(status().isOk()); + verify(userService).deleteUser(any()); + } + + @Test + public void shouldReturn404WhenUserNotFoundAndDeleteUser() throws Exception { + doThrow(new NotFoundException("Not Found")).when(userService).deleteUser(1000L); + mockMvc.perform(delete("/users/1000")).andExpect(status().isNotFound()); + verify(userService).deleteUser(any()); + } + + @Test + public void shouldReturn400WhenNegativeIdsAndDeleteUser() throws Exception { + mockMvc.perform(delete("/users/-1")).andExpect(status().isBadRequest()); + verify(userService, never()).deleteUser(any()); + } + + @Test + public void shouldReturn400WhenZeroUserIdAndDeleteUser() throws Exception { + mockMvc.perform(delete("/users/0")).andExpect(status().isBadRequest()); + verify(userService, never()).deleteUser(any()); + } } @Nested From b59e0401ae100181c4857223932ee598f2829acf Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:14:51 +0300 Subject: [PATCH 24/26] fix: checkstyle --- .../practicum/filmorate/controller/FilmControllerTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java index c674d91..9939f11 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java @@ -8,7 +8,6 @@ import org.springframework.http.MediaType; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.Genre; import ru.yandex.practicum.filmorate.model.Mpa; import ru.yandex.practicum.filmorate.service.film.FilmService; From ca57da2ab9d745260e37a3437b93440503e726f9 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 20 Nov 2025 17:43:09 +0300 Subject: [PATCH 25/26] feat: add db scheme --- Filmorate.png | Bin 0 -> 73601 bytes README.md | 11 +++++++++++ 2 files changed, 11 insertions(+) create mode 100644 Filmorate.png diff --git a/Filmorate.png b/Filmorate.png new file mode 100644 index 0000000000000000000000000000000000000000..744dd23f6127a8d1eae56db73a98bf6cdbea2672 GIT binary patch literal 73601 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�@t~c~iRpN^5#J^$nK({KJ!m5{5 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 G_x=w<3BBV0 literal 0 HcmV?d00001 diff --git a/README.md b/README.md index c198fee..273776a 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,17 @@ Filmorate - это RESTful API для управления фильмами и пользователями. Сейчас приложение позволяет добавлять, обновлять и просматривать информацию о фильмах и пользователях. +## Схема базы данных +(![Схема базы данных](Filmorate.png)) + +- #### films - хранит все фильмы +- #### users - хранит всех пользователей +- #### mpa - хранит MPA рейтинг +- #### genres - хранит жанры +- #### films_genres - связующая таблица между фильмами и жанрами +- #### likes - хранит лайки фильма +- #### friendships - хранит друзей пользователя + ## Модели данных ### Film From b33a5bceca3da14225628dd3574d842274b214fd Mon Sep 17 00:00:00 2001 From: Crodi Date: Fri, 21 Nov 2025 10:05:56 +0300 Subject: [PATCH 26/26] fix: add field encapsulation --- .../yandex/practicum/filmorate/controller/GenreController.java | 2 +- .../ru/yandex/practicum/filmorate/controller/MpaController.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java index 45673bc..2ee59a0 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java @@ -14,7 +14,7 @@ @RequestMapping("/genres") public class GenreController { - GenreService service; + private final GenreService service; public GenreController(GenreService service) { this.service = service; diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java index 20c8ff9..8343f8d 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java @@ -14,7 +14,7 @@ @RequestMapping("/mpa") public class MpaController { - MpaService service; + private final MpaService service; public MpaController(MpaService service) { this.service = service;