diff --git a/README.md b/README.md index 3eee4ad..762899d 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,58 @@ Учебный проект. Созданиие приложений на основе шаблона "Spring"
-## Спринт 10 -Добавляем контроллеры фильмов и пользователей.
+## Спринт 12 +Добавляем базу данных. +![схема базы данных](/schema.png) -## Спринт 11 -Добавляем работу с "друзьями" и "лайками" \ No newline at end of file +### Описание таблиц базы данных + +1. **users** - таблица описания пользователей.
+поля: + - первичный ключ *id* - идентификатор подьзователя; + - *email* - адрес электронно почты пользователя; + - *login* - логин пользоателя; + - *name* - имя пользователя; + - *birthday* - дата рождения пользователя; + +
+2. **friends** - таблица связи с "друзьями" пользователя.
+ поля: + - *user_id* - идентификатор пользователя (отсылает к таблице *users*) - идентификатор пользователя; + - *friend_id* - идентификатор друга (отсылает к таблице *users*) - идентификатор пользователя; + - *confirmed* - флаг подтвержденной дружбы (если "дружба" двусторонняя); + +
+3. **genre** - таблица описания жанро фильма.
+ поля: + - первичный ключ *id* - идентификатор жанра; + - *name* - наименование жанра; + +
+4. **MPA** - таблица описания рейтингов Ассоциации кинокомпаний (MPA).
+ поля: + - первичный ключ *id* - идентификатор рейтинга; + - *name* - буквенный код рейтинга (G, PG, PG-13, R, NC-17); + - *description* - описание рейтинга; + +
+5. **films** - таблица описания фильмов.
+ поля: + - первичный ключ *id* - идентификатор фильма; + - *name* - название фильма; + - *description* - описание фильма; + - *releaseDAte* - бата выпуска фильма; + - *len_min* - длительность фильма в минутах; + - *MPA_id* -рейтинг MPA. (отсылает к таблице *MPA*) - идентификатор рейтинга; + +
+6. **film_genre** - таблица определения жанров фильма.
+ поля: + - *film_id* - идентификатор фильма (отсылает к таблице *films*); + - *genre_id* - идентификатор жанра (отсылает к таблице *genre*); + +
+7. **likes** - таблица "лайков" пользователей.
+ поля: + - *user_id* - идентификатор пользователя (отсылает к таблице *users*); + - *film_id* - идентификатор фильма (отсылает к таблице *films*); diff --git a/pom.xml b/pom.xml index 0a23621..a4c8f5c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.4.1 ru.yandex.practicum @@ -17,26 +17,40 @@ 21 + com.google.code.gson gson + + org.projectlombok + lombok + provided + + + + com.h2database + h2 + 2.3.232 + + org.springframework.boot spring-boot-starter-web + 3.4.1 - org.projectlombok - lombok - provided + org.springframework.boot + spring-boot-starter-jdbc + 3.4.1 org.springframework.boot spring-boot-starter-test - test + 3.4.2 diff --git a/schema.png b/schema.png new file mode 100644 index 0000000..5263bd6 Binary files /dev/null and b/schema.png differ diff --git a/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java b/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java index 643c0c2..615ea3a 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java +++ b/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java @@ -9,12 +9,12 @@ @SpringBootApplication public class FilmorateApplication { - /** - * Запуск приложения. - * - * @param args - параметры запуска. - */ - public static void main(final String[] args) { - SpringApplication.run(FilmorateApplication.class, args); - } + /** + * Запуск приложения. + * + * @param args - параметры запуска. + */ + public static void main(final String[] args) { + SpringApplication.run(FilmorateApplication.class, args); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java b/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java index c3ef798..2276139 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java @@ -2,6 +2,7 @@ import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataAccessException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -9,11 +10,12 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; +import ru.yandex.practicum.filmorate.exception.InternalServerException; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.ErrorMessage; -import ru.yandex.practicum.filmorate.model.ValidationErrorResponse; -import ru.yandex.practicum.filmorate.model.Violation; +import ru.yandex.practicum.filmorate.validator.ValidationErrorResponse; +import ru.yandex.practicum.filmorate.validator.Violation; import java.util.List; import java.util.stream.Collectors; @@ -85,6 +87,13 @@ public ErrorMessage notFoundObject(NotFoundException exception) { return new ErrorMessage(exception.getMessage()); } + @ExceptionHandler + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorMessage notFoundData(DataAccessException exception) { + return new ErrorMessage(exception.getMessage()); + } + + /** * Обработка исключения HttpMessageNotReadableException при поступлении пустого запроса * @@ -101,6 +110,14 @@ public ResponseEntity onHttpMessageNotReadableException( .body(new ErrorMessage("В запросе отсутствуют необходимые данные.")); } + + @ExceptionHandler + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorMessage internalException(final InternalServerException e) { + log.warn("Error", e); + return new ErrorMessage(e.getMessage()); + } + /** * Обработка непредвиденного исключения * 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 e343f1a..e94ed7c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -1,13 +1,14 @@ package ru.yandex.practicum.filmorate.controller; +import jakarta.validation.constraints.Min; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.model.Marker; import ru.yandex.practicum.filmorate.service.FilmService; +import ru.yandex.practicum.filmorate.validator.Marker; import java.util.Collection; import java.util.Map; @@ -52,7 +53,7 @@ public Film findFilm(@PathVariable Integer id) { } @GetMapping("/popular") - public Collection findPopularFilms(@RequestParam(defaultValue = "10") int count) { + public Collection findPopularFilms(@RequestParam(defaultValue = "10") @Min(1) int count) { log.info("Ищем популярные {} фильмов.", count); return service.findPopularFilms(count); } 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..d9c870d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java @@ -0,0 +1,37 @@ +package ru.yandex.practicum.filmorate.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.service.GenreService; + +import java.util.Collection; + +@Slf4j +@RestController +@RequestMapping("/genres") +public class GenreController { + + private final GenreService genreService; + + public GenreController(GenreService genreService) { + this.genreService = genreService; + } + + @GetMapping + @ResponseStatus(HttpStatus.OK) + public Collection findAllGenres() { + log.info("Запрашиваем список всех жанров."); + return genreService.getAllGenres(); + } + + @GetMapping("/{id}") + @ResponseStatus(HttpStatus.OK) + public Genre findGenreById(@PathVariable int id) { + log.info("Ищем жанр id={}.", id); + return genreService.getGenreById(id); + } + +} + 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..b2b4d64 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java @@ -0,0 +1,36 @@ +package ru.yandex.practicum.filmorate.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.model.Mpa; +import ru.yandex.practicum.filmorate.service.MpaService; + +import java.util.Collection; + +@Slf4j +@RestController +@RequestMapping("/mpa") +public class MpaController { + + private final MpaService mpaService; + + public MpaController(MpaService mpaService) { + this.mpaService = mpaService; + } + + @GetMapping + @ResponseStatus(HttpStatus.OK) + public Collection findAllMpa() { + log.info("Запрашиваем список всех рейтинков MPA."); + return mpaService.filndAllMpa(); + } + + @GetMapping("/{id}") + @ResponseStatus(HttpStatus.OK) + public Mpa findMpaById(@PathVariable int id) { + log.info("Ищем рейтинг id={}.", id); + return mpaService.findMpa(id); + } + +} 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 250a3e4..51e2f8d 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -5,9 +5,9 @@ import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import ru.yandex.practicum.filmorate.model.Marker; import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.service.UserService; +import ru.yandex.practicum.filmorate.validator.Marker; import java.util.Collection; @@ -59,7 +59,7 @@ public User findUser(@PathVariable Integer id) { @GetMapping("/{id}/friends") public Collection findUsersFriends(@PathVariable Integer id) { log.info("Ищем друзей пользователя id={}.", id); - return service.getUsersFriends(id); + return service.getUserFriends(id); } /** diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/InternalServerException.java b/src/main/java/ru/yandex/practicum/filmorate/exception/InternalServerException.java new file mode 100644 index 0000000..2cce47d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/InternalServerException.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.exception; + +public class InternalServerException extends RuntimeException { + public InternalServerException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmGenreRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmGenreRowMapper.java new file mode 100644 index 0000000..85f0f59 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmGenreRowMapper.java @@ -0,0 +1,20 @@ +package ru.yandex.practicum.filmorate.mapper; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.FilmGenre; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class FilmGenreRowMapper implements RowMapper { + @Override + public FilmGenre mapRow(ResultSet resultSet, int rowNum) throws SQLException { + FilmGenre filmGenre = new FilmGenre(); + filmGenre.setFilmId(resultSet.getInt("film_id")); + filmGenre.setGenreId(resultSet.getInt("genre_id")); + filmGenre.setGenreName(resultSet.getString("genre_name")); + return filmGenre; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmRowMapper.java new file mode 100644 index 0000000..9d87952 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmRowMapper.java @@ -0,0 +1,22 @@ +package ru.yandex.practicum.filmorate.mapper; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Film; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class FilmRowMapper implements RowMapper { + @Override + public Film mapRow(ResultSet resultSet, int rowNum) throws SQLException { + Film film = new Film(); + film.setId(resultSet.getInt("id")); + film.setName(resultSet.getString("name")); + film.setDescription(resultSet.getString("description")); + film.setReleaseDate(resultSet.getDate(4).toLocalDate()); + film.setDuration(resultSet.getInt("len_min")); + return film; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/GenreRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/GenreRowMapper.java new file mode 100644 index 0000000..46a75dd --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/GenreRowMapper.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.mapper; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Genre; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class GenreRowMapper implements RowMapper { + @Override + public Genre mapRow(ResultSet resultSet, int rowNum) throws SQLException { + Genre genre = new Genre(); + genre.setId(resultSet.getInt("id")); + genre.setName(resultSet.getString("name")); + return genre; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/MpaRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/MpaRowMapper.java new file mode 100644 index 0000000..5100e36 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/MpaRowMapper.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.mapper; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class MpaRowMapper implements RowMapper { + public Mpa mapRow(ResultSet rs, int rowNum) throws SQLException { + Mpa mpa = new Mpa(); + mpa.setId(rs.getInt("id")); + mpa.setName(rs.getString("name")); + mpa.setDescription(rs.getString("description")); + return mpa; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/UserRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/UserRowMapper.java new file mode 100644 index 0000000..ab46955 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/UserRowMapper.java @@ -0,0 +1,23 @@ +package ru.yandex.practicum.filmorate.mapper; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.User; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class UserRowMapper implements RowMapper { + + @Override + public User mapRow(ResultSet resultSet, int rowNum) throws SQLException { + User user = new User(); + user.setId(resultSet.getInt("id")); + user.setEmail(resultSet.getString("email")); + user.setLogin(resultSet.getString("login")); + user.setName(resultSet.getString("name")); + user.setBirthday(resultSet.getDate(5).toLocalDate()); + return user; + } +} 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 9fd9b6b..06792cc 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -1,25 +1,26 @@ package ru.yandex.practicum.filmorate.model; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.ToString; -import org.springframework.validation.annotation.Validated; +import ru.yandex.practicum.filmorate.validator.LegalFilmDate; +import ru.yandex.practicum.filmorate.validator.Marker; import java.time.LocalDate; +import java.util.LinkedHashSet; /** * Класс описания фильма. */ @Data -@ToString(callSuper = false) -@EqualsAndHashCode(exclude = {"id", "description"}) // при сравнении не учитывать: id, description -@AllArgsConstructor -@Validated -public class Film extends StorageData { +@EqualsAndHashCode(of = {"name", "releaseDate", "mpa", "genres"}) +public class Film { + + @NotNull(groups = {Marker.OnUpdate.class}, message = "id должен быть определен") + protected Integer id; @NotBlank(message = "Название фильма не может быть пустым.", groups = {Marker.OnBasic.class, Marker.OnUpdate.class}) @@ -36,5 +37,22 @@ public class Film extends StorageData { groups = {Marker.OnBasic.class, Marker.OnUpdate.class}) private int duration; - private Integer rank = 0; + // рейтинг Ассоциации кинокомпаний + @NotNull(groups = {Marker.OnBasic.class}, message = "рейтинг MPA должен быть определен") + private Mpa mpa; + + // жанры фильма + private LinkedHashSet genres = new LinkedHashSet<>(); + + public void addGenre(Genre genre) { + genres.add(genre); + } + + public void removeGenre(Genre genre) { + genres.remove(genre); + } + + public void clearGenres() { + genres.clear(); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/FilmGenre.java b/src/main/java/ru/yandex/practicum/filmorate/model/FilmGenre.java new file mode 100644 index 0000000..6eefaef --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/FilmGenre.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.Data; + +/** + * Сопоставление идентификатора фильма с соответствующим жанром + */ +@Data +public class FilmGenre { + private Integer filmId; + private Integer genreId; + private String genreName; +} 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..4a7ed25 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java @@ -0,0 +1,15 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@EqualsAndHashCode(of = {"id"}) +@NoArgsConstructor +@AllArgsConstructor +public class Genre { + private int 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..ea7f17a --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Mpa.java @@ -0,0 +1,23 @@ +package ru.yandex.practicum.filmorate.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@EqualsAndHashCode(of = {"id"}) +@NoArgsConstructor +@AllArgsConstructor +public class Mpa { + private int id; + private String name; + + @JsonIgnore + private String description; + + public Mpa(int id) { + this.id = id; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/StorageData.java b/src/main/java/ru/yandex/practicum/filmorate/model/StorageData.java deleted file mode 100644 index 5a8bb54..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/model/StorageData.java +++ /dev/null @@ -1,13 +0,0 @@ -package ru.yandex.practicum.filmorate.model; - -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -/** - * Класс данных для наследования классов модели - */ -@Data -public class StorageData { - @NotNull(groups = {Marker.OnUpdate.class}, message = "id должен быть определен") - protected Integer id; -} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/User.java b/src/main/java/ru/yandex/practicum/filmorate/model/User.java index fd9ad3f..3bdf6af 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/User.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java @@ -1,14 +1,12 @@ package ru.yandex.practicum.filmorate.model; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.PastOrPresent; -import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.*; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.ToString; +import lombok.NoArgsConstructor; import org.springframework.validation.annotation.Validated; +import ru.yandex.practicum.filmorate.validator.Marker; import java.time.LocalDate; @@ -16,11 +14,14 @@ * Класс описания пользователя. */ @Data -@ToString(callSuper = false) -@EqualsAndHashCode(exclude = {"id", "name", "birthday"}) +@EqualsAndHashCode(of = {"email"}) +@NoArgsConstructor @AllArgsConstructor @Validated -public class User extends StorageData { +public class User { + + @NotNull(groups = {Marker.OnUpdate.class}, message = "id должен быть определен") + protected Integer id; @NotBlank(message = "Email не может быть пустым", groups = Marker.OnBasic.class) @Email(message = "Email должен удовлетворять правилам формирования почтовых адресов.", @@ -37,4 +38,11 @@ public class User extends StorageData { @PastOrPresent(message = "Дата рождения не может быть в будущем.", groups = {Marker.OnBasic.class, Marker.OnUpdate.class}) private LocalDate birthday; + + public User(String email, String login, String name, LocalDate birthday) { + this.email = email; + this.login = login; + this.name = name; + this.birthday = birthday; + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java index fc5634c..062e029 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -1,134 +1,27 @@ package ru.yandex.practicum.filmorate.service; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import ru.yandex.practicum.filmorate.exception.NotFoundException; -import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.storage.film.FilmStorage; -import ru.yandex.practicum.filmorate.storage.user.UserStorage; import java.util.Collection; -import java.util.HashMap; import java.util.Map; -/** - * Класс реализации запросов к информации о фильмах - */ -@Slf4j -@Service -public class FilmService { +public interface FilmService { - private final FilmStorage films; - private final UserStorage users; + Collection findAllFilms(); - public FilmService(FilmStorage filmStorage, UserStorage users) { - this.films = filmStorage; - this.users = users; - } + Film getFilmById(Integer id); - /** - * Метод поиска всех фильмов - * - * @return - список фильмов - */ - public Collection findAllFilms() { - return films.findAllFilms(); - } + Film addNewFilm(Film film); - /** - * Метод поиска фильма по идентификатору - * - * @param id - идентификатор - * @return - найденный фильм - */ - public Film getFilmById(Integer id) { - return films.getFilmById(id).orElseThrow(() -> - new NotFoundException("Не найден фильм id=" + id)); - } + Film updateFilm(Film updFilm); - /** - * Метод добавления нового фильма. - * - * @param film - объект для добавления - * @return - подтверждение добавленного объекта - */ - public Film addNewFilm(Film film) { - if (films.findAllFilms().contains(film)) { - throw new ValidationException("Фильм уже существует :" - + film.getName()); - } - return films.addNewFilm(film); - } + String onDelete(); - /** - * Метод обновления информации о фильме. - * - * @param updFilm - объект с обновленной информацией о фильме - * @return - подтверждение обновленного объекта - */ - public Film updateFilm(Film updFilm) { - Integer id = updFilm.getId(); - Film film = films.getFilmById(id).orElseThrow(() -> - new NotFoundException("Не найден фильм id=" + id)); + Integer addNewLike(Integer filmId, Integer userId); - // Обновляем информаию во временном объекте - if (updFilm.getName() != null) { - film.setName(updFilm.getName()); - } - if (updFilm.getDescription() != null) { - film.setDescription(updFilm.getDescription()); - } - if (updFilm.getReleaseDate() != null) { - film.setReleaseDate(updFilm.getReleaseDate()); - } - if (updFilm.getDuration() > 0) { - film.setDuration(updFilm.getDuration()); - } - return film; - } + Integer removeLike(Integer filmId, Integer userId); - /** - * Удаление всех фильмов - * - * @return - сообщение о выполнении - */ - public String onDelete() { - films.removeAllFilms(); - return "Все фильмы удалены."; - } + Collection findPopularFilms(int count); - public Integer addNewLike(Integer filmId, Integer userId) { - Film film = films.getFilmById(filmId).orElseThrow(() -> - new NotFoundException("Не найден фильм id=" + filmId)); - users.getUserById(userId).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + userId)); - - film.setRank(films.addNewLike(filmId, userId)); - return film.getRank(); - } - - public Integer removeLike(Integer filmId, Integer userId) { - Film film = films.getFilmById(filmId).orElseThrow(() -> - new NotFoundException("Не найден фильм id=" + filmId)); - users.getUserById(userId).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + userId)); - - film.setRank(films.removeLike(filmId, userId)); - return film.getRank(); - } - - public Collection findPopularFilms(int count) { - return films.findPopularFilms(count); - } - - public Map getFilmRank(Integer filmId) { - Film film = films.getFilmById(filmId).orElseThrow(() -> - new NotFoundException("Не найден фильм id=" + filmId)); - - Map response = new HashMap<>(); - response.put("Фильм ", film.getName()); - response.put("Рейтинг", film.getRank().toString()); - return response; - } + Map getFilmRank(Integer filmId); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java new file mode 100644 index 0000000..9563115 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java @@ -0,0 +1,151 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.InternalServerException; +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.user.UserStorage; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * Класс реализации запросов к информации о фильмах + */ +@Service +public class FilmServiceImpl implements FilmService { + + private final FilmStorage films; + private final UserStorage users; + + public FilmServiceImpl(FilmStorage filmStorage, UserStorage users) { + this.films = filmStorage; + this.users = users; + } + + /** + * Метод поиска всех фильмов + * + * @return - список фильмов + */ + @Override + public Collection findAllFilms() { + return films.findAllFilms(); + } + + /** + * Метод поиска фильма по идентификатору + * + * @param id - идентификатор + * @return - найденный фильм + */ + @Override + public Film getFilmById(Integer id) { + return films.getFilmById(id) + .orElseThrow(() -> new NotFoundException("Не найден фильм id=" + id)); + } + + /** + * Метод добавления нового фильма. + * + * @param film - объект для добавления + * @return - подтверждение добавленного объекта + */ + @Override + public Film addNewFilm(Film film) { + Optional existingFilm = films.findAllFilms().stream() + .filter(film1 -> film1.equals(film)) + .findFirst(); + if (existingFilm.isPresent()) { + throw new ValidationException("Фильм уже существует: " + existingFilm.get()); + } + return films.addNewFilm(film); + } + + /** + * Метод обновления информации о фильме. + * + * @param updFilm - объект с обновленной информацией о фильме + * @return - подтверждение обновленного объекта + */ + @Override + public Film updateFilm(Film updFilm) { + Integer id = updFilm.getId(); + Film film = films.getFilmById(id) + .orElseThrow(() -> new NotFoundException("Не найден фильм id=" + id)); + + if (updFilm.getName() != null) { + film.setName(updFilm.getName()); + } + if (updFilm.getDescription() != null) { + film.setDescription(updFilm.getDescription()); + } + if (updFilm.getReleaseDate() != null) { + film.setReleaseDate(updFilm.getReleaseDate()); + } + if (updFilm.getDuration() > 0) { + film.setDuration(updFilm.getDuration()); + } + if (updFilm.getMpa() != null) { + film.setMpa(updFilm.getMpa()); + } + if (updFilm.getGenres().size() > 0) { + film.setGenres(updFilm.getGenres()); + } + films.updateFilm(film); + + return films.getFilmById(id).orElseThrow(() -> + new InternalServerException("Ошибка обновления фильма id=" + id)); + } + + /** + * Удаление всех фильмов + * + * @return - сообщение о выполнении + */ + @Override + public String onDelete() { + films.removeAllFilms(); + return "Все фильмы удалены."; + } + + @Override + public Integer addNewLike(Integer filmId, Integer userId) { + films.getFilmById(filmId).orElseThrow(() -> + new NotFoundException("Не найден фильм id=" + filmId)); + users.getUserById(userId).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + userId)); + + return films.addNewLike(filmId, userId); + } + + @Override + public Integer removeLike(Integer filmId, Integer userId) { + Film film = films.getFilmById(filmId).orElseThrow(() -> + new NotFoundException("Не найден фильм id=" + filmId)); + users.getUserById(userId).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + userId)); + + return films.removeLike(filmId, userId); + } + + @Override + public Collection findPopularFilms(int count) { + return films.findPopularFilms(count); + } + + @Override + public Map getFilmRank(Integer filmId) { + Film film = films.getFilmById(filmId).orElseThrow(() -> + new NotFoundException("Не найден фильм id=" + filmId)); + + Map response = new HashMap<>(); + response.put("Фильм ", film.getName()); + response.put("лайков", films.getFilmRank(filmId).toString()); + return response; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java b/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java new file mode 100644 index 0000000..b76126a --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java @@ -0,0 +1,12 @@ +package ru.yandex.practicum.filmorate.service; + +import ru.yandex.practicum.filmorate.model.Genre; + +import java.util.Collection; + +public interface GenreService { + + Collection getAllGenres(); + + Genre getGenreById(int id); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/GenreServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/GenreServiceImpl.java new file mode 100644 index 0000000..b4d27a4 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/GenreServiceImpl.java @@ -0,0 +1,28 @@ +package ru.yandex.practicum.filmorate.service; + +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.genre.GenreStorage; + +import java.util.Collection; + +@Service +public class GenreServiceImpl implements GenreService { + private final GenreStorage genereStorage; + + public GenreServiceImpl(GenreStorage genereStorage) { + this.genereStorage = genereStorage; + } + + @Override + public Collection getAllGenres() { + return genereStorage.findAllGenres(); + } + + @Override + public Genre getGenreById(int id) { + return genereStorage.findGenre(id).orElseThrow(() -> + new NotFoundException("Не найден жанр id=" + id)); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java b/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java new file mode 100644 index 0000000..4c27704 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java @@ -0,0 +1,14 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.util.Collection; + +@Service +public interface MpaService { + + Collection filndAllMpa(); + + Mpa findMpa(Integer id); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/MpaServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/MpaServiceImpl.java new file mode 100644 index 0000000..44064d6 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/MpaServiceImpl.java @@ -0,0 +1,28 @@ +package ru.yandex.practicum.filmorate.service; + +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.mpa.MpaStorage; + +import java.util.Collection; + +@Service +public class MpaServiceImpl implements MpaService { + private final MpaStorage mpaStorage; + + public MpaServiceImpl(MpaStorage mpaStorage) { + this.mpaStorage = mpaStorage; + } + + @Override + public Collection filndAllMpa() { + return mpaStorage.findAllMpa(); + } + + @Override + public Mpa findMpa(Integer id) { + return mpaStorage.findMpa(id).orElseThrow(() -> + new NotFoundException("Не найден рейтинг id=" + id)); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java index 6007441..ab230c7 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -1,182 +1,26 @@ package ru.yandex.practicum.filmorate.service; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import ru.yandex.practicum.filmorate.exception.NotFoundException; -import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.User; -import ru.yandex.practicum.filmorate.storage.user.UserStorage; -import java.util.ArrayList; import java.util.Collection; -import java.util.List; -/** - * Класс реализации запросов к информации о пользователях - */ -@Slf4j -@Service -public class UserService { +public interface UserService { + Collection findAllUsers(); - private final UserStorage users; + User addNewUser(User user); - @Autowired - public UserService(UserStorage userStorage) { - this.users = userStorage; - } + User getUserById(Integer id); - /** - * Метод поиска всех пользователей - * - * @return - список пользователей - */ - public Collection findAllUsers() { - return users.findAllUsers(); - } + User updateUser(User updUser); - /** - * Метод добавления нового пользователя. - * - * @param user - объект для добавления - * @return - подтверждение добавленного объекта - */ - public User addNewUser(User user) { - // "имя для отображения может быть пустым - // — в таком случае будет использован логин" (ТЗ-№10) - if (user.getName() == null | user.getName().isBlank()) { - user.setName(user.getLogin()); - } - if (users.findAllUsers().contains(user)) { - throw new ValidationException("Пользователь уже существует " - + user.getEmail()); - } - return users.addNewUser(user); - } + String removeAllUsers(); - /** - * Метод чтения информации о пользователе по заданному идентификатору - * - * @param id - идентификатор пользователя - * @return - найденный объект - */ - public User getUserById(Integer id) { - User user = users.getUserById(id).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + id)); - return user; - } + void addFriends(Integer id1, Integer id2); - /** - * Метод обновления информации о пользователе. - * При вызове метода промзводится проверка аннотаций только для маркера OnUpdate.class. - * Кроме id любой другой параметр может отсутствовать - * - * @param updUser - объект с обновленной информацией о пользователе - * @return - обновленный объект - */ - public User updateUser(User updUser) { - Integer id = updUser.getId(); - User user = users.getUserById(id).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + id)); + void breakUpFriends(Integer id1, Integer id2); - // Обновляем информаию во временном объекте - if (updUser.getEmail() != null) { - user.setEmail(updUser.getEmail()); - } - if (updUser.getLogin() != null) { - user.setLogin(updUser.getLogin()); - } - if (updUser.getName() != null) { - user.setName(updUser.getName()); - } - if (updUser.getBirthday() != null) { - user.setBirthday(updUser.getBirthday()); - } - return user; - } + Collection getUserFriends(Integer userId); - /** - * Удаление всех пользователей - * - * @return - сообщение о выполнении - */ - public String removeAllUsers() { - log.debug("Sevice: Удаляем всех пользователей."); - users.removeAllUsers(); - return "Все пользователи удалены."; - } + Collection getCommonFriends(Integer id1, Integer id2); - /** - * Медод добавления пользователей в друзья - * добавление в друзья происходит взаимное без подтверждений - * - * @param id1 - идентификатор пользователя - * @param id2 - идентификатор друга - */ - public void addFriends(Integer id1, Integer id2) { - users.getUserById(id1).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + id1)); - users.getUserById(id2).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + id2)); - - // Добавление в друзья происходит без подтверждения. - // Еслb id1 дружит с id2, то автоматически id2 дружит с id1 - users.addFriend(id1, id2); - users.addFriend(id2, id1); - } - - /** - * Метод удаления пользователя из "друзей" - * - * @param id1 - идентификатор пользователя - * @param id2 - идентификатор друга - * @return - сообщение о подтверждении - */ - public void breakUpFriends(Integer id1, Integer id2) { - users.getUserById(id1).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + id1)); - users.getUserById(id2).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + id2)); - - users.breakUpFriends(id1, id2); - } - - /** - * Поиск всех друзей пользователя - * - * @param userId - идентификатор пользователя - * @return - список друзей - */ - public Collection getUsersFriends(Integer userId) { - users.getUserById(userId).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + userId)); - - List friends = new ArrayList<>(); - for (Integer friendId : users.findAllFriends(userId)) { - friends.add(users.getUserById(friendId).get()); - } - return friends; - } - - /** - * Метод поиска общих друзей пользователей - * - * @param id1 - идентификатор пользователя - * @param id2 - идентификатор другого пользователя - * @return - список общих друзей - */ - public Collection getCommonFriends(Integer id1, Integer id2) { - users.getUserById(id1).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + id1)); - users.getUserById(id2).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + id2)); - - List friendsId = users.findAllFriends(id1); - friendsId.retainAll(users.findAllFriends(id2)); - List friends = new ArrayList<>(); - for (Integer id : friendsId) { - friends.add(users.getUserById(id).get()); - } - return friends; - } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java new file mode 100644 index 0000000..7161159 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java @@ -0,0 +1,178 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ValidationException; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; + +import java.util.Collection; + +/** + * Класс реализации запросов к информации о пользователях + */ +@Slf4j +@Service +public class UserServiceImpl implements UserService { + + private final UserStorage users; + + public UserServiceImpl(UserStorage users) { + this.users = users; + } + + /** + * Метод поиска всех пользователей + * + * @return - список пользователей + */ + @Override + public Collection findAllUsers() { + return users.findAllUsers(); + } + + /** + * Метод добавления нового пользователя. + * + * @param user - объект для добавления + * @return - подтверждение добавленного объекта + */ + @Override + public User addNewUser(User user) { + // "имя для отображения может быть пустым + // — в таком случае будет использован логин" (ТЗ-№10) + if (user.getName() == null | user.getName().isBlank()) { + user.setName(user.getLogin()); + } + if (users.findAllUsers().contains(user)) { + throw new ValidationException("Пользователь уже существует " + + user.getEmail()); + } + return users.addNewUser(user); + } + + /** + * Метод чтения информации о пользователе по заданному идентификатору + * + * @param id - идентификатор пользователя + * @return - найденный объект + */ + @Override + public User getUserById(Integer id) { + User user = users.getUserById(id).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id)); + return user; + } + + /** + * Метод обновления информации о пользователе. + * При вызове метода промзводится проверка аннотаций только для маркера OnUpdate.class. + * Кроме id любой другой параметр может отсутствовать + * + * @param updUser - объект с обновленной информацией о пользователе + * @return - обновленный объект + */ + @Override + public User updateUser(User updUser) { + Integer id = updUser.getId(); + User user = users.getUserById(id).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id)); + + // Обновляем информаию во временном объекте + if (updUser.getEmail() != null) { + user.setEmail(updUser.getEmail()); + } + if (updUser.getLogin() != null) { + user.setLogin(updUser.getLogin()); + } + if (updUser.getName() != null) { + user.setName(updUser.getName()); + } + if (updUser.getBirthday() != null) { + user.setBirthday(updUser.getBirthday()); + } + + users.updateUser(user); + return user; + } + + /** + * Удаление всех пользователей + * + * @return - сообщение о выполнении + */ + @Override + public String removeAllUsers() { + log.debug("Sevice: Удаляем всех пользователей."); + users.removeAllUsers(); + return "Все пользователи удалены."; + } + + /** + * Медод добавления пользователей в друзья + * добавление в друзья происходит взаимное без подтверждений + * + * @param id1 - идентификатор пользователя + * @param id2 - идентификатор друга + */ + @Override + public void addFriends(Integer id1, Integer id2) { + users.getUserById(id1).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id1)); + users.getUserById(id2).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id2)); + + // Добавление в друзья + users.addFriend(id1, id2); + } + + /** + * Метод удаления пользователя из "друзей" + * + * @param id1 - идентификатор пользователя + * @param id2 - идентификатор друга + * @return - сообщение о подтверждении + */ + @Override + public void breakUpFriends(Integer id1, Integer id2) { + users.getUserById(id1).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id1)); + users.getUserById(id2).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id2)); + + users.breakUpFriends(id1, id2); + } + + /** + * Поиск всех друзей пользователя + * + * @param userId - идентификатор пользователя + * @return - список друзей + */ + @Override + public Collection getUserFriends(Integer userId) { + users.getUserById(userId).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + userId)); + + return users.getUserFriends(userId); + } + + /** + * Поиск общих друзей пользователей + * + * @param id1 - идентификатор пользователя + * @param id2 - идентификатор другого пользователя + * @return - список общих друзей + */ + @Override + public Collection getCommonFriends(Integer id1, Integer id2) { + users.getUserById(id1).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id1)); + users.getUserById(id2).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id2)); + + return users.getCommonFriends(id1, id2); + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java new file mode 100644 index 0000000..d9c100e --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java @@ -0,0 +1,329 @@ +package ru.yandex.practicum.filmorate.storage.film; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.exception.InternalServerException; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.mapper.FilmGenreRowMapper; +import ru.yandex.practicum.filmorate.mapper.FilmRowMapper; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.FilmGenre; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.*; + +@Repository +public class FilmDbStorage implements FilmStorage { + + private static final String SQL_INSERT_FILM = "INSERT INTO films (name, description, releasedate, len_min, mpa_id)" + + " VALUES ( :name, :description, :releasedate, :len_min, :mpa_id)"; + private static final String SQL_UPDATE_GENRES = "MERGE INTO films_genres (film_id, genre_id) " + + " VALUES (:film_id, :genre_id)"; + + private static final String SQL_UPDATE_FILM = "UPDATE films SET name = :name, description = :description, " + + "releasedate = :releasedate, len_min = :len_min, mpa_id = :mpa_id WHERE id = :id"; + private static final String SQL_ADD_LIKE = "MERGE INTO likes (user_id, film_id) VALUES (:userId, :filmId)"; + private static final String SQL_REMOVE_LIKE = "DELETE FROM likes WHERE user_id = :userId AND film_id = :filmId"; + private static final String SQL_FIND_ALL_FILMS = "SELECT f.*, mpa.name as mpa_name FROM films AS f " + + " INNER JOIN mpa ON f.mpa_id = mpa.id"; + private static final String SQL_FIND_FILM_BY_ID = "SELECT f.*, mpa.name as mpa_name, fg.genre_id, g.name AS genre_name\n" + + " FROM (films AS f INNER JOIN mpa ON f.MPA_ID = mpa.ID)\n" + + " LEFT JOIN (films_genres AS fg INNER JOIN genres AS g ON fg.GENRE_ID = g.ID) ON fg.film_id = f.id\n" + + " WHERE f.id = :id"; + private static final String SQL_FIND_POPULAR_FILMS = "SELECT f.*, mpa.name AS mpa_name, popular.count_film\n" + + "FROM (films AS f INNER JOIN mpa ON f.MPA_ID = mpa.ID\n)" + + " LEFT OUTER JOIN\n" + + " (SELECT film_id, count(film_id) as count_film\n" + + " FROM LIKES GROUP BY film_id) AS popular\n" + + " ON f.id = popular.film_id\n" + + "ORDER BY popular.count_film DESC\n"; + + @Autowired + private NamedParameterJdbcTemplate jdbc; + + /** + * Добавление информации о фильме + * + * @param newFilm - объект для добавления + * @return - подтвержденный объект + */ + @Override + public Film addNewFilm(Film newFilm) { + GeneratedKeyHolder generatedKeyHolder = new GeneratedKeyHolder(); + + // сохраняем информацию о фильме в базу данных + try { + jdbc.update(SQL_INSERT_FILM, + new MapSqlParameterSource() + .addValue("name", newFilm.getName()) + .addValue("description", newFilm.getDescription()) + .addValue("releasedate", newFilm.getReleaseDate(), Types.DATE) + .addValue("len_min", newFilm.getDuration()) + .addValue("mpa_id", newFilm.getMpa().getId()), + generatedKeyHolder + ); + } catch (DataAccessException e) { + throw new NotFoundException("Получены недопустимые параметры запроса: " + + e.getMessage()); + } + + // получаем идентификатор фильма + final Integer filmId = generatedKeyHolder.getKey().intValue(); + newFilm.setId(filmId); + + // добавляем жанры Фильма Если определены + if (!newFilm.getGenres().isEmpty()) { + SqlParameterSource[] batch = newFilm.getGenres().stream() + .map(genre -> new MapSqlParameterSource() + .addValue("film_id", filmId) + .addValue("genre_id", genre.getId())) + .toArray(SqlParameterSource[]::new); + jdbc.batchUpdate(SQL_UPDATE_GENRES, batch); + } + + return getFilmById(filmId).orElseThrow(() -> + new InternalServerException("Ошибка при добавлении фильма.")); + } + + /** + * Поиск фильма по идентификатору + * + * @param id - идентификатор фильма + * @return - объект описания фильма + */ + @Override + public Optional getFilmById(Integer id) { + try { + Film film = jdbc.query(SQL_FIND_FILM_BY_ID, + new MapSqlParameterSource() + .addValue("id", id), + new ResultSetExtractor() { + @Override + public Film extractData(ResultSet rs) throws SQLException, DataAccessException { + rs.next(); + Film filmRs = new FilmRowMapper().mapRow(rs, 1); + Integer mpaId = rs.getInt("mpa_id"); + if (mpaId != null) { + Mpa mpa = new Mpa(); + mpa.setId(mpaId); + mpa.setName(rs.getString("mpa_name")); + filmRs.setMpa(mpa); + } + do { + Integer genreId = rs.getInt("genre_id"); + if (genreId != 0) { + Genre genre = new Genre(); + genre.setId(genreId); + genre.setName(rs.getString("genre_name")); + filmRs.addGenre(genre); + } + } while (rs.next()); + return filmRs; + } + } + ); + return Optional.ofNullable(film); + + } catch (DataAccessException ignored) { + return Optional.empty(); + } + } + + /** + * Поиск всех фильмов + * + * @return - список фильмов + */ + @Override + public Collection findAllFilms() { + return findFilmsByQuery(SQL_FIND_ALL_FILMS); + } + + /** + * Поиск популярных фильмов + * + * @param count - количество фильмов в итоговом списке + * @return - список самых популярных фильмов + */ + @Override + public Collection findPopularFilms(int count) { + if (count > 0) { + return findFilmsByQuery(SQL_FIND_POPULAR_FILMS + " LIMIT " + count); + } + return findFilmsByQuery(SQL_FIND_POPULAR_FILMS); + } + + /** + * Обновление сведений о фильме + * + * @param updFilm - объект c информацией для обновления. id должен быть определен + */ + @Override + public void updateFilm(Film updFilm) { + // задаем параметры SQL запоса + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("name", updFilm.getName()); + params.addValue("description", updFilm.getDescription()); + params.addValue("releasedate", updFilm.getReleaseDate(), Types.DATE); + params.addValue("len_min", updFilm.getDuration()); + params.addValue("mpa_id", updFilm.getMpa().getId()); + params.addValue("id", updFilm.getId()); + + // обновляем информацию о фильме + int rowsUpdated = jdbc.update(SQL_UPDATE_FILM, params); + if (rowsUpdated == 0) { + throw new InternalServerException("Не удалось обновить информацию о фильие"); + } + + // Удаляем все жанры которые были определены для фильма + int filmId = updFilm.getId(); + jdbc.update("DELETE FROM films_genres WHERE film_id = :filmId", + new MapSqlParameterSource() + .addValue("filmId", filmId)); + + // добавляем жанры Фильма если определены новые + if (!updFilm.getGenres().isEmpty()) { + SqlParameterSource[] batch = updFilm.getGenres().stream() + .map(genre -> new MapSqlParameterSource() + .addValue("film_id", updFilm.getId()) + .addValue("genre_id", genre.getId())) + .toArray(SqlParameterSource[]::new); + jdbc.batchUpdate(SQL_UPDATE_GENRES, batch); + } + } + + /** + * Добавление "лайка" к фильму. + * + * @param filmId - идентификатор фильма + * @param userId - идентификатор пользователя + * @return - число "лайков" + */ + @Override + public Integer addNewLike(Integer filmId, Integer userId) { + int rowsUpdated = jdbc.update(SQL_ADD_LIKE, new MapSqlParameterSource() + .addValue("userId", userId) + .addValue("filmId", filmId) + ); + if (rowsUpdated == 0) { + throw new InternalServerException("Не удалось обновить данные"); + } + return getFilmRank(filmId); + } + + /** + * Удаление "лайка" у фильма. + * + * @param filmId - идентификатор фильма + * @param userId - идентификатор пользователя + * @return - число "лайков" + */ + @Override + public Integer removeLike(Integer filmId, Integer userId) { + jdbc.update(SQL_REMOVE_LIKE, new MapSqlParameterSource() + .addValue("userId", userId) + .addValue("filmId", filmId) + ); + return getFilmRank(filmId); + } + + /** + * Подсчет "лайков" фильма. + * + * @param filmId - идентификатор фильма + * @return - число "лайков" + */ + @Override + public Integer getFilmRank(Integer filmId) { + try { + return jdbc.queryForObject("SELECT count(film_id) FROM likes WHERE film_id = :filmId", + new MapSqlParameterSource() + .addValue("filmId", filmId), + Integer.class); + } catch (EmptyResultDataAccessException ignored) { + throw new NotFoundException("Информация о популярности фильма не найдена. id:" + filmId); + } + } + + /** + * Удаление всех фильмов + */ + @Override + public void removeAllFilms() { + jdbc.update("DELETE FROM likes", new MapSqlParameterSource() + .addValue("table", "likes")); + jdbc.update("DELETE FROM films_genres", new MapSqlParameterSource() + .addValue("table", "films_genres")); + jdbc.update("DELETE FROM films", new MapSqlParameterSource() + .addValue("table", "films")); + } + + /** + * Метод поиска фильмов заполнения их соответствующими жанрами + * + * @param sqlQueryFilms - строка SQL запроса для выборки всех полей объекта Film + * @return - коллекция фильмов. + */ + private Collection findFilmsByQuery(String sqlQueryFilms) { + try { + // Загружаем из базы данных информацию о фильмах + Map filmsMap; + filmsMap = jdbc.query(sqlQueryFilms, + new ResultSetExtractor>() { + @Override + public Map extractData(ResultSet rs) + throws SQLException, DataAccessException { + Map fMap = new LinkedHashMap<>(); + while (rs.next()) { + Film film = new FilmRowMapper().mapRow(rs, 1); + Integer mpaId = rs.getInt("mpa_id"); + if (mpaId != 0) { + Mpa mpa = new Mpa(); + mpa.setId(mpaId); + mpa.setName(rs.getString("mpa_name")); + film.setMpa(mpa); + } + fMap.put(film.getId(), film); + } + return fMap; + } + }); + // Если ничего не нашли, то возвращаем пустой список + if (filmsMap.isEmpty()) { + return List.of(); + } + + // Загружаем из базы данных все ссылки на жанры + List filmsGenres; + filmsGenres = jdbc.query( + "SELECT fg.*, g.name AS genre_name FROM films_genres AS fg INNER JOIN genres AS g ON fg.GENRE_ID = g.ID", + new FilmGenreRowMapper()); + + // пополням фильмы сведениями о жанрах + for (FilmGenre filmGenre : filmsGenres) { + int filmId = filmGenre.getFilmId(); + if (filmsMap.keySet().contains(filmId)) { + Genre genre = new Genre(); + genre.setId(filmGenre.getGenreId()); + genre.setName(filmGenre.getGenreName()); + filmsMap.get(filmId).addGenre(genre); + } + } + return filmsMap.values(); + } catch (EmptyResultDataAccessException ignored) { + return List.of(); + } + } + +} 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 826408c..7216873 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 @@ -27,5 +27,8 @@ public interface FilmStorage { // удаление "лайка" к фильму Integer removeLike(Integer filmId, Integer userId); + // Чтение числа "лайков" у филььма + Integer getFilmRank(Integer filmId); + void removeAllFilms(); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java deleted file mode 100644 index 814cfc0..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java +++ /dev/null @@ -1,128 +0,0 @@ -package ru.yandex.practicum.filmorate.storage.film; - -import org.springframework.stereotype.Component; -import ru.yandex.practicum.filmorate.model.Film; - -import java.util.*; - -@Component -public class InMemoryFilmStorage implements FilmStorage { - - private final Map films = new HashMap<>(); - private final Map> likes = new HashMap<>(); - private final List filmsRating = new ArrayList<>(); - Integer filmId = 0; - - @Override - public Film addNewFilm(Film film) { - filmId++; - film.setId(filmId); - film.setRank(0); - films.put(filmId, film); - likes.put(filmId, new HashSet<>()); - filmsRating.add(film); - return film; - } - - @Override - public Optional getFilmById(Integer id) { - return Optional.ofNullable(films.get(id)); - } - - @Override - public Collection findAllFilms() { - return films.values(); - } - - @Override - public void updateFilm(Film updFilm) { - films.put(updFilm.getId(), updFilm); - } - - /** - * Добавление "лайка" к фильму. - * - * @param filmId - идентифмкатор фильма - * @param userId - идентификатор пользователя - * @return - число никальных лайков - */ - @Override - public Integer addNewLike(Integer filmId, Integer userId) { - likes.get(filmId).add(userId); - Film film = films.get(filmId); - film.setRank(likes.get(filmId).size()); - setFilmsRating(film); - return likes.get(filmId).size(); - } - - /** - * Удаление "лайка" у фильма - * - * @param filmId - идентификатор фильма - * @param userId - идентификатор пользователя - * @return - число независимых "лайков" у фильма - */ - @Override - public Integer removeLike(Integer filmId, Integer userId) { - likes.get(filmId).remove(userId); - Film film = films.get(filmId); - film.setRank(likes.get(filmId).size()); - setFilmsRating(film); - return likes.get(filmId).size(); - } - - /** - * Определение позиции фильма в рейтинге. - * Так как рейтинг представляет собой уже упорядоченный список, - * то сортировать весь список нет смысла. - * Нужно уточнить место в рейтинге заданного объекта. - * - * @param film - */ - private void setFilmsRating(Film film) { - int ratingSize = filmsRating.size(); - - // Если фильмов меньше двух, то ничего не делаем - if (ratingSize < 2) { - return; - } - int index = filmsRating.indexOf(film); - - // Проверяем изменение рейтинга на возрастание - while ((index > 0) && - (film.getRank() > filmsRating.get(index - 1).getRank())) { - filmsRating.set(index, filmsRating.get(index - 1)); - filmsRating.set(--index, film); - } - - // Проверяем изменение рейтинга на убывание - while (index < (ratingSize - 1) && - (film.getRank() < filmsRating.get(index + 1).getRank())) { - filmsRating.set(index, filmsRating.get(index + 1)); - filmsRating.set(++index, film); - } - } - - /** - * Поиск самых популярных фильмов - * - * @param count - количество фильмов для поиска - * @return - список фильмов - */ - @Override - public Collection findPopularFilms(int count) { - if (count > filmsRating.size()) { - count = filmsRating.size(); - } - return filmsRating.subList(0, count); - - } - - @Override - public void removeAllFilms() { - likes.clear(); - filmsRating.clear(); - films.clear(); - filmId = 0; - } -} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorage.java new file mode 100644 index 0000000..f7e4f37 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorage.java @@ -0,0 +1,56 @@ +package ru.yandex.practicum.filmorate.storage.genre; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.mapper.GenreRowMapper; +import ru.yandex.practicum.filmorate.model.Genre; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +@Repository +public class GenreDbStorage implements GenreStorage { + + private static final String SQL_GET_ALL_GENRES = "SELECT * FROM genres"; + private static final String SQL_GET_GENRE = "SELECT * FROM genres WHERE id = :id"; + + @Autowired + private NamedParameterJdbcTemplate jdbc; + + /** + * Чтение всех жанров в справочнике + * + * @return + */ + @Override + public Collection findAllGenres() { + try { + return jdbc.query(SQL_GET_ALL_GENRES, new GenreRowMapper()); + } catch (EmptyResultDataAccessException ignored) { + return List.of(); + } + } + + /** + * чтение жанра по идентификатору + * + * @param id - идентификатор жанра + * @return - объект Optional + */ + @Override + public Optional findGenre(Integer id) { + try { + Genre genre = jdbc.queryForObject(SQL_GET_GENRE, + new MapSqlParameterSource() + .addValue("id", id), + new GenreRowMapper()); + return Optional.ofNullable(genre); + } catch (EmptyResultDataAccessException ignored) { + return Optional.empty(); + } + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreStorage.java new file mode 100644 index 0000000..5931bd8 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreStorage.java @@ -0,0 +1,12 @@ +package ru.yandex.practicum.filmorate.storage.genre; + +import ru.yandex.practicum.filmorate.model.Genre; + +import java.util.Collection; +import java.util.Optional; + +public interface GenreStorage { + Collection findAllGenres(); + + Optional findGenre(Integer id); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorage.java new file mode 100644 index 0000000..c51f4ec --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorage.java @@ -0,0 +1,44 @@ +package ru.yandex.practicum.filmorate.storage.mpa; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.mapper.MpaRowMapper; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +@Repository +public class MpaDbStorage implements MpaStorage { + private static final String SQL_GET_ALL_MPA = "SELECT * FROM mpa"; + private static final String SQL_GET_MPA = "SELECT * FROM mpa WHERE id = :id"; + + @Autowired + private NamedParameterJdbcTemplate jdbc; + + @Override + public Collection findAllMpa() { + try { + return jdbc.query(SQL_GET_ALL_MPA, new MpaRowMapper()); + } catch (EmptyResultDataAccessException ignored) { + return List.of(); + } + } + + @Override + public Optional findMpa(Integer id) { + try { + Mpa mpa = jdbc.queryForObject(SQL_GET_MPA, + new MapSqlParameterSource() + .addValue("id", id), + new MpaRowMapper()); + return Optional.ofNullable(mpa); + } catch (EmptyResultDataAccessException ignored) { + return Optional.empty(); + } + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaStorage.java new file mode 100644 index 0000000..4efc2d4 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaStorage.java @@ -0,0 +1,12 @@ +package ru.yandex.practicum.filmorate.storage.mpa; + +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.util.Collection; +import java.util.Optional; + +public interface MpaStorage { + Collection findAllMpa(); + + Optional findMpa(Integer id); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java deleted file mode 100644 index 6e9f9fa..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java +++ /dev/null @@ -1,61 +0,0 @@ -package ru.yandex.practicum.filmorate.storage.user; - -import org.springframework.stereotype.Component; -import ru.yandex.practicum.filmorate.model.User; - -import java.util.*; - -@Component -public class InMemoryUserStorage implements UserStorage { - - private final Map users = new HashMap<>(); - private final Map> friends = new HashMap<>(); - private Integer userId = 0; - - @Override - public User addNewUser(User user) { - userId++; - user.setId(userId); - users.put(userId, user); - friends.put(userId, new HashSet<>()); - return user; - } - - @Override - public Optional getUserById(Integer id) { - return Optional.ofNullable(users.get(id)); - } - - @Override - public Collection findAllUsers() { - return users.values(); - } - - @Override - public void updateUser(User updUser) { - users.put(updUser.getId(), updUser); - } - - @Override - public void removeAllUsers() { - friends.clear(); - users.clear(); - userId = 0; - } - - @Override - public void addFriend(Integer userId, Integer friendId) { - friends.get(userId).add(friendId); - } - - @Override - public void breakUpFriends(Integer id1, Integer id2) { - friends.get(id1).remove(id2); - friends.get(id2).remove(id1); - } - - @Override - public List findAllFriends(Integer userId) { - return new ArrayList<>(friends.get(userId)); - } -} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java new file mode 100644 index 0000000..6414f24 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java @@ -0,0 +1,193 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.exception.InternalServerException; +import ru.yandex.practicum.filmorate.mapper.UserRowMapper; +import ru.yandex.practicum.filmorate.model.User; + +import java.sql.Types; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +@Repository +public class UserDbStorage implements UserStorage { + + private static final String SQL_INSERT_USER = "INSERT INTO users (email, login, name, birthday) VALUES (:email, :login, :name, :birthday)"; + private static final String SQL_FIND_USER = "SELECT * FROM users WHERE id = :id"; + private static final String SQL_UPDATE_USER = "UPDATE users SET email = :email, login = :login, name = :name, birthday = :birthday WHERE id = :id"; + private static final String SQL_ADD_FRIEND = "MERGE INTO friends (user_id, friend_id, confirmed) VALUES (:userId, :friendId, FALSE)"; + private static final String SQL_REMOVE_FRIEND = "DELETE FROM friends WHERE (user_id = :userId) AND (friend_id = :friendId)"; + + @Autowired + private NamedParameterJdbcTemplate jdbc; + + /** + * Добавление в базу нового пользователя + * + * @param newUser - объект для добавления + * @return - подтвержденный объект + */ + @Override + public User addNewUser(User newUser) { + // для доступа к сгенерированому ключу новой записи создаем объект GeneratedKeyHolder + GeneratedKeyHolder generatedKeyHolder = new GeneratedKeyHolder(); + + jdbc.update(SQL_INSERT_USER, + new MapSqlParameterSource() + .addValue("email", newUser.getEmail()) + .addValue("login", newUser.getLogin()) + .addValue("name", newUser.getName()) + .addValue("birthday", newUser.getBirthday(), Types.DATE), + generatedKeyHolder + ); + + // присваиваем сгенерирванный ключ записи в качестве идентификатора пользователя + newUser.setId(generatedKeyHolder.getKey().intValue()); + return newUser; + } + + /** + * Поиск пользователя по идентификатору. + * + * @param id - идентификатор пользователя + * @return - Optional + */ + @Override + public Optional getUserById(Integer id) { + try { + User user = jdbc.queryForObject(SQL_FIND_USER, + new MapSqlParameterSource() + .addValue("id", id), + new UserRowMapper()); + return Optional.ofNullable(user); + } catch (DataAccessException ignored) { + return Optional.empty(); + } + } + + /** + * Поиск всех пользователей + * + * @return - список пользователей + */ + @Override + public Collection findAllUsers() { + try { + return jdbc.query("SELECT * FROM users", new UserRowMapper()); + } catch (EmptyResultDataAccessException ignored) { + return List.of(); + } + } + + /** + * Обновление сведений о пользователе + * + * @param updUser - объект с обновленной информацией + */ + @Override + public void updateUser(User updUser) { + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("email", updUser.getEmail()); + params.addValue("login", updUser.getLogin()); + params.addValue("name", updUser.getName()); + params.addValue("birthday", updUser.getBirthday(), Types.DATE); + params.addValue("id", updUser.getId()); + + int rowsUpdated = jdbc.update(SQL_UPDATE_USER, params); + if (rowsUpdated == 0) { + throw new InternalServerException("Не удалось обновить данные"); + } + } + + /** + * Удаление всех пользователей + */ + @Override + public void removeAllUsers() { + jdbc.update("DELETE FROM likes", new MapSqlParameterSource() + .addValue("table", "likes")); + jdbc.update("DELETE FROM friends", new MapSqlParameterSource() + .addValue("table", "friends")); + jdbc.update("DELETE FROM users", new MapSqlParameterSource() + .addValue("table", "users")); + } + + /** + * Добавление "друга" + * + * @param userId - идентификатор пользователя + * @param friendId - идентификатор друга + */ + @Override + public void addFriend(Integer userId, Integer friendId) { + jdbc.update(SQL_ADD_FRIEND, new MapSqlParameterSource() + .addValue("userId", userId) + .addValue("friendId", friendId) + ); + } + + /** + * Исключение из "друзей" + * + * @param userId - идентификатор пользователя + * @param friendsId - идентификатор друга + */ + @Override + public void breakUpFriends(Integer userId, Integer friendsId) { + jdbc.update(SQL_REMOVE_FRIEND, new MapSqlParameterSource() + .addValue("userId", userId) + .addValue("friendId", friendsId) + ); + } + + /** + * Поиск друзей пользователя. + * + * @param userId - идентификатор пользователя + * @return - список друзей + */ + @Override + public Collection getUserFriends(Integer userId) { + String sql = "SELECT * FROM users WHERE id IN " + + "(SELECT friend_id FROM friends WHERE user_id = :userId)"; + try { + return jdbc.query(sql, new MapSqlParameterSource() + .addValue("userId", userId), + new UserRowMapper() + ); + } catch (EmptyResultDataAccessException ignored) { + return List.of(); + } + } + + /** + * Поиск общих друзей + * + * @param id1 - идентификатор первого пользователя + * @param id2 - идентификатор второго пользователя + * @return - спмсок общих друзей + */ + @Override + public Collection getCommonFriends(Integer id1, Integer id2) { + String sql = "SELECT * FROM users WHERE id IN (SELECT tu.friend_id " + + "FROM (SELECT * FROM friends WHERE user_id = :userId1) AS tu " + + "INNER JOIN (SELECT * FROM friends WHERE user_id = :userId2) AS tf " + + "WHERE tu.friend_id = tf.friend_id)"; + try { + return jdbc.query(sql, new MapSqlParameterSource() + .addValue("userId1", id1) + .addValue("userId2", id2), + new UserRowMapper()); + } catch (EmptyResultDataAccessException ignored) { + return List.of(); + } + } + +} 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 3fa46c4..094492d 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,7 +3,6 @@ import ru.yandex.practicum.filmorate.model.User; import java.util.Collection; -import java.util.List; import java.util.Optional; public interface UserStorage { @@ -24,5 +23,7 @@ public interface UserStorage { void breakUpFriends(Integer id1, Integer id2); - List findAllFriends(Integer userId); + Collection getUserFriends(Integer userId); + + Collection getCommonFriends(Integer id1, Integer id2); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/LegalFilmDate.java b/src/main/java/ru/yandex/practicum/filmorate/validator/LegalFilmDate.java similarity index 92% rename from src/main/java/ru/yandex/practicum/filmorate/model/LegalFilmDate.java rename to src/main/java/ru/yandex/practicum/filmorate/validator/LegalFilmDate.java index 6a48a1c..d0d9105 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/LegalFilmDate.java +++ b/src/main/java/ru/yandex/practicum/filmorate/validator/LegalFilmDate.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.model; +package ru.yandex.practicum.filmorate.validator; import jakarta.validation.Constraint; diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/LegalFilmDateValidator.java b/src/main/java/ru/yandex/practicum/filmorate/validator/LegalFilmDateValidator.java similarity index 94% rename from src/main/java/ru/yandex/practicum/filmorate/model/LegalFilmDateValidator.java rename to src/main/java/ru/yandex/practicum/filmorate/validator/LegalFilmDateValidator.java index 23eae05..99b44b3 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/LegalFilmDateValidator.java +++ b/src/main/java/ru/yandex/practicum/filmorate/validator/LegalFilmDateValidator.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.model; +package ru.yandex.practicum.filmorate.validator; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/LocalDateAdapter.java b/src/main/java/ru/yandex/practicum/filmorate/validator/LocalDateAdapter.java similarity index 95% rename from src/main/java/ru/yandex/practicum/filmorate/model/LocalDateAdapter.java rename to src/main/java/ru/yandex/practicum/filmorate/validator/LocalDateAdapter.java index 3722b3c..6f015e2 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/LocalDateAdapter.java +++ b/src/main/java/ru/yandex/practicum/filmorate/validator/LocalDateAdapter.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.model; +package ru.yandex.practicum.filmorate.validator; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Marker.java b/src/main/java/ru/yandex/practicum/filmorate/validator/Marker.java similarity index 87% rename from src/main/java/ru/yandex/practicum/filmorate/model/Marker.java rename to src/main/java/ru/yandex/practicum/filmorate/validator/Marker.java index e595677..665d46f 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Marker.java +++ b/src/main/java/ru/yandex/practicum/filmorate/validator/Marker.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.model; +package ru.yandex.practicum.filmorate.validator; /** * Описание интерфейсов групп проверки аннотаций diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/ValidationErrorResponse.java b/src/main/java/ru/yandex/practicum/filmorate/validator/ValidationErrorResponse.java similarity index 90% rename from src/main/java/ru/yandex/practicum/filmorate/model/ValidationErrorResponse.java rename to src/main/java/ru/yandex/practicum/filmorate/validator/ValidationErrorResponse.java index 08d0619..42e9cf7 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/ValidationErrorResponse.java +++ b/src/main/java/ru/yandex/practicum/filmorate/validator/ValidationErrorResponse.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.model; +package ru.yandex.practicum.filmorate.validator; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Violation.java b/src/main/java/ru/yandex/practicum/filmorate/validator/Violation.java similarity index 88% rename from src/main/java/ru/yandex/practicum/filmorate/model/Violation.java rename to src/main/java/ru/yandex/practicum/filmorate/validator/Violation.java index e28b642..68db96c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Violation.java +++ b/src/main/java/ru/yandex/practicum/filmorate/validator/Violation.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.model; +package ru.yandex.practicum.filmorate.validator; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 28aacc1..aa40b46 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,4 +1,8 @@ -server.port=8080 +spring.sql.init.mode=always +spring.datasource.url=jdbc:h2:mem:filmorate +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password +server.port=8080 logging.level.org.zalando.logbook: TRACE - diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 0000000..4af8e5e --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,17 @@ +-- Заполняем справочник жанров +MERGE INTO genres (id, name) + VALUES ( 1, 'Комедия'), + (2, 'Драма'), + (3, 'Мультфильм'), + (4, 'Триллер'), + (5, 'Документальный'), + (6, 'Боевик'); + +-- Заполняем справочник рейтингов MPA +MERGE INTO MPA (id, name, description) + VALUES (1, 'G', 'у фильма нет возрастных ограничений'), + (2, 'PG', 'детям рекомендуется смотреть фильм с родителями'), + (3, 'PG-13', 'детям до 13 лет просмотр не желателен'), + (4, 'R', 'лицам до 17 лет просматривать фильм можно только в присутствии взрослого'), + (5, 'NC-17', 'лицам до 18 лет просмотр запрещён'); + diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..a6d0924 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,53 @@ +-- Создаем таблицу пользователей +CREATE TABLE IF NOT EXISTS users ( + id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + login VARCHAR(40) NOT NULL, + name VARCHAR(40) NOT NULL, + birthday DATE NOT NULL +); + +-- Создаем таблицу друзей +CREATE TABLE IF NOT EXISTS friends ( + user_id INTEGER NOT NULL REFERENCES users(id), + friend_id INTEGER NOT NULL REFERENCES users(id), + confirmed BOOLEAN NOT NULL DEFAULT FALSE, + PRIMARY KEY (user_id, friend_id) +); + +-- Создаем справочник жанров фильма +CREATE TABLE IF NOT EXISTS genres ( + id INTEGER PRIMARY KEY, + name VARCHAR(40) NOT NULL +); + +-- Создаем справочник рейтинга MPA +CREATE TABLE IF NOT EXISTS MPA ( + id INTEGER PRIMARY KEY, + name VARCHAR(8) NOT NULL, + description VARCHAR(80) +); + +-- Создаем таблицу описания фильма +CREATE TABLE IF NOT EXISTS films ( + id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name VARCHAR(40) NOT NULL, + description VARCHAR(200), + releaseDate DATE, + len_min INTEGER, + MPA_id INTEGER NOT NULL REFERENCES MPA(id) +); + +-- Создаем таблицу описания жанра фильма +CREATE TABLE IF NOT EXISTS films_genres ( + film_id INTEGER NOT NULL REFERENCES films(id), + genre_id INTEGER NOT NULL REFERENCES genres(id), + PRIMARY KEY (film_id, genre_id) +); + +-- Создаем таблицу "лайков" к фильмам +CREATE TABLE IF NOT EXISTS likes ( + user_id INTEGER NOT NULL REFERENCES users(id), + film_id INTEGER NOT NULL REFERENCES films(id), + PRIMARY KEY (user_id, film_id) +); diff --git a/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java b/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java index 660412e..02f0bf6 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java +++ b/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java @@ -1,13 +1,13 @@ package ru.yandex.practicum.filmorate; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest +@AutoConfigureTestDatabase class FilmorateApplicationTests { - - @Test - void contextLoads() { - } - + @Test + void contextLoads() throws Exception { + } } 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 cc3de5e..2b24a07 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java @@ -2,67 +2,106 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import org.junit.jupiter.api.BeforeEach; +import com.google.gson.reflect.TypeToken; +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.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.model.LocalDateAdapter; -import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Mpa; +import ru.yandex.practicum.filmorate.validator.LocalDateAdapter; import java.time.LocalDate; +import java.util.List; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** - * Тестируем контроллер запросов о фильмах + * Тестируем контроллер запросов работы с данными о фильмах + *

+ * Для успешного выполнения тестов, при инициализации базы данных + * должна быть подготовлена информация о четырех тестовых фильмах и + * о четырех тестовых пользователях. + * Файл первоначальных данных ./src/test/resources/data.sql */ @SpringBootTest @AutoConfigureMockMvc +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) class FilmControllerTest { + private static final int TEST_FILM_ID = 1; + @Autowired MockMvc mvc; - static Gson gson = new GsonBuilder() + Gson gson = new GsonBuilder() .setPrettyPrinting() .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) .create(); + // Определяем тип сериализации списка + class FilmListTypeToken extends TypeToken> { + } + /** - * Перед каждым тестом очищаем список фильмов. + * Генерация информации о тестовом фильме + * Поля должны соответствовать содержимому базы данных для фильма TEST_FILM_ID + * + * @return - объект, который ожидается для TEST_FILM_ID */ - @BeforeEach - void setUp() throws Exception { - mvc.perform(delete("/films")) - .andExpect(status().isOk()); - - mvc.perform(delete("/users")) - .andExpect(status().isOk()); - - // Создадим одного пользователя для "лайков" - User user = new User("User1234@domain", - "user1234", "test user", - LocalDate.now().minusYears(22)); - - String jsonString = gson.toJson(user); - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); + static Film getTestFilm() { + Film film = new Film(); + film.setId(TEST_FILM_ID); + film.setName("TestFilmName"); + film.setDescription("TestFilmDescription"); + film.setReleaseDate(LocalDate.of(2001, 2, 3)); + film.setDuration(51); + film.setMpa(new Mpa(1)); + film.addGenre(new Genre(1, "Комедия")); + return film; } /** - * Тестируем режим поиска фильмов. + * Тестируем поиск всех фильмов */ @Test void findAllFilms() throws Exception { - makeFilms(3); - mvc.perform(get("/films")) - .andExpect(status().isOk()); + MvcResult result = mvc.perform(get("/films")) + .andExpect(status().isOk()) // ожидается код статус 200 + .andReturn(); + List films = gson.fromJson(result.getResponse().getContentAsString(), + new FilmListTypeToken().getType()); + assertTrue(!films.isEmpty(), + "Список фильмов пуст."); + } + + /** + * Тестируем поиск фильма по идентификатору + */ + @Test + void findFilm() throws Exception { + // попытка поиска несуществующего фильма + mvc.perform(get("/films/10000")) + .andExpect(status().isNotFound()); // ожидается код статус 404 + + // поиск тестового фильма + MvcResult result = mvc.perform(get("/films/" + TEST_FILM_ID)) + .andExpect(status().isOk()) // ожидается код статус 200 + .andReturn(); + Film filmDb = gson.fromJson(result.getResponse().getContentAsString(), Film.class); + assertThat(filmDb) + .withFailMessage("Считанный объект не соответствует ожидаемому.") + .isEqualTo(getTestFilm()); } /** @@ -70,25 +109,21 @@ void findAllFilms() throws Exception { */ @Test void addNewFilm() throws Exception { - Film film = new Film("Film Test1", - "Testing addNewFilm", - LocalDate.now().minusYears(10), - 60, 0); + Film film = getTestFilm(); + film.setId(null); + film.setName("NewTestFilm"); String jsonString = gson.toJson(film); // При успешном добавлении фильма - // должен возвращаться статус 200 "Ok" - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); - - // При повторном добавлении фильма - // должен возвращаться статус 400 "BadRequest" - mvc.perform(post("/films") + // должен возвращаться статус 201 "Created" + MvcResult result = mvc.perform(post("/films") .content(jsonString) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); + .andExpect(status().isCreated()) + .andReturn(); + Film filmDb = gson.fromJson(result.getResponse().getContentAsString(), Film.class); + assertNotNull(filmDb.getId(), + "при добавлении фильма должен присваиваться ненулевой идентификатор."); } /** @@ -96,19 +131,26 @@ void addNewFilm() throws Exception { */ @Test void updateFilm() throws Exception { - Film film = new Film("Film Test2", - "Testing updateFilm", - LocalDate.now().minusYears(10), - 60, 0); + Film film = getTestFilm(); + film.setId(null); + film.setName("TestFilmForUpdate"); String jsonString = gson.toJson(film); - // Добавляем тестовый фильм - mvc.perform(post("/films") + // При успешном добавлении фильма + // должен возвращаться статус 201 "Created" + MvcResult result = mvc.perform(post("/films") .content(jsonString) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); + .andExpect(status().isCreated()) + .andReturn(); + Film filmDb = gson.fromJson(result.getResponse().getContentAsString(), Film.class); + + // Готовим информацию для обновления + film.setName("TestFilmNameUpdated"); + film.setDescription("Description updated"); + film.addGenre(new Genre(3, "Мультфильм")); + jsonString = gson.toJson(film); - film.setDescription("Updated."); jsonString = gson.toJson(film); // При обновлении фильма с отсутствующим id // должен возвращаться статус 400 "BadRequest" @@ -117,7 +159,6 @@ void updateFilm() throws Exception { .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isBadRequest()); - film.setId(1000); jsonString = gson.toJson(film); // При обновлении фильма с неверным id @@ -127,14 +168,19 @@ void updateFilm() throws Exception { .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNotFound()); - film.setId(1); + film.setId(filmDb.getId()); jsonString = gson.toJson(film); // При обновлении фильма с корректным id // должен возвращаться статус 200 "Ok" - mvc.perform(put("/films") + result = mvc.perform(put("/films") .content(jsonString) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andReturn(); + filmDb = gson.fromJson(result.getResponse().getContentAsString(), Film.class); + assertThat(filmDb) + .withFailMessage("Обновленный объект не соответствует ожидаемому.") + .isEqualTo(film); } /** @@ -144,61 +190,53 @@ void updateFilm() throws Exception { */ @Test void addLike() throws Exception { - makeFilms(3); - // При добавлении "лайка" от несуществующего пользователя // должен возвращаться статус 404 "NotFound" mvc.perform(put("/films/1/like/1000") - .content("") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNotFound()); // При добавлении "лайка" // должен возвращаться статус 200 "Ok" - mvc.perform(put("/films/2/like/1") - .content("") + mvc.perform(put("/films/1/like/1") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); } /** * Тестируем удаление "лайка" - * - * @throws Exception */ @Test - void deleteLike() throws Exception { + void removeLike() throws Exception { addLike(); // При удалении "лайка" // должен возвращаться статус 200 "Ok" - mvc.perform(delete("/films/2/like/1") - .content("") + mvc.perform(delete("/films/1/like/1") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); } - /** - * Генерация тестовых фильмов - * - * @param count - количество фильмов - * @throws Exception - */ - void makeFilms(int count) throws Exception { - StringBuilder fBuilder = new StringBuilder(); - fBuilder.append("{\"name\": \"Film%d\","); - fBuilder.append("\"description\": \"description%d\","); - fBuilder.append("\"releaseDate\": \"2000-01-%02d\","); - fBuilder.append("\"duration\": %d}"); - String formatStr = fBuilder.toString(); - - for (int i = 1; i <= count; i++) { - String jsonString = String.format(formatStr, i, i, i, i * 10); - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); - } - + @Test + void findPopularFilms() throws Exception { + mvc.perform(put("/films/1/like/1").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + mvc.perform(put("/films/2/like/1").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + mvc.perform(put("/films/2/like/2").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + mvc.perform(put("/films/3/like/1").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + mvc.perform(put("/films/3/like/3").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + mvc.perform(put("/films/3/like/2").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + mvc.perform(put("/films/3/like/4").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + mvc.perform(put("/films/4/like/2").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + mvc.perform(put("/films/4/like/3").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + mvc.perform(put("/films/4/like/3").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + + MvcResult result = mvc.perform(get("/films/popular?count=2")) + .andExpect(status().isOk()) // ожидается код статус 200 + .andReturn(); + List filmsPopular = gson.fromJson(result.getResponse().getContentAsString(), + new FilmListTypeToken().getType()); + assertTrue(filmsPopular.size() == 2, + "Число популярных фильмов не соответствует ожидаемому"); } -} + +} \ No newline at end of file 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..84245c1 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/GenreControllerTest.java @@ -0,0 +1,94 @@ +package ru.yandex.practicum.filmorate.controller; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeAll; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import ru.yandex.practicum.filmorate.model.Genre; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Тестирование контроллера запросов работы со справочником жанров фильа + *

+ * Для успешного выполнения тестов, при инициализации базы данных + * должндолжен быть полностью заполнен справочник жанров. + * Файл первоначальных данных ./src/test/resources/data.sql + */ +@SpringBootTest +@AutoConfigureMockMvc +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class GenreControllerTest { + + private static final List testGenres = new ArrayList<>(); + + @Autowired + MockMvc mvc; + + // Определяем тип сериализации списка + class GenreListTypeToken extends TypeToken> { + } + + Gson gson = new GsonBuilder() + .setPrettyPrinting() + .create(); + + /** + * Инициализация эталонного списка жанров. + */ + @BeforeAll + static void setUp() { + testGenres.add(new Genre(1, "Комедия")); + testGenres.add(new Genre(2, "Драма")); + testGenres.add(new Genre(3, "Мультфильм")); + testGenres.add(new Genre(4, "Триллер")); + testGenres.add(new Genre(5, "Документальный")); + testGenres.add(new Genre(6, "Боевик")); + } + + /** + * Тестируем список всех жанров + */ + @Test + void findAllGenres() throws Exception { + MvcResult result = mvc.perform(get("/genres")) + .andExpect(status().isOk()) + .andReturn(); + + List genres = gson.fromJson(result.getResponse().getContentAsString(), + new GenreListTypeToken().getType()); + for (Genre genre : genres) { + assertTrue(testGenres.contains(genre), + "Получен неизвестный жанр: " + genre.toString()); + } + } + + /** + * Тестируем поиск жанра по идентификатору + */ + @Test + void findGenreById() throws Exception { + for (Genre genre : testGenres) { + MvcResult result = mvc.perform(get("/genres/" + genre.getId())) + .andExpect(status().isOk()) + .andReturn(); + Genre genreDb = gson.fromJson(result.getResponse().getContentAsString(), Genre.class); + assertTrue(genre.equals(genreDb), + "Получен неизвестный жанр: " + genreDb.toString()); + } + } +} \ No newline at end of file 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..602f723 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/MpaControllerTest.java @@ -0,0 +1,98 @@ +package ru.yandex.practicum.filmorate.controller; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeAll; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Тестирование контроллера запросов работы со справочником жанров рейтингов MPA + *

+ * Для успешного выполнения тестов, при инициализации базы данных + * должндолжен быть полностью заполнен справочник рейтингов. + * Файл первоначальных данных ./src/test/resources/data.sql + */ +@SpringBootTest +@AutoConfigureMockMvc +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class MpaControllerTest { + private static List testMpaList = new ArrayList<>(); + + private static final List testGenres = new ArrayList<>(); + + @Autowired + MockMvc mvc; + + // Определяем тип сериализации списка + class MpaListTypeToken extends TypeToken> { + } + + Gson gson = new GsonBuilder() + .setPrettyPrinting() + .create(); + + /** + * Инициализация эталонного списка рейтингов. + */ + @BeforeAll + static void setUp() { + testMpaList.add(new Mpa(1, "G", "у фильма нет возрастных ограничений")); + testMpaList.add(new Mpa(2, "PG", "детям рекомендуется смотреть фильм с родителями")); + testMpaList.add(new Mpa(3, "PG-13", "детям до 13 лет просмотр не желателен")); + testMpaList.add(new Mpa(4, "R", "лицам до 17 лет просматривать фильм можно только в присутствии взрослого")); + testMpaList.add(new Mpa(5, "NC-17", "лицам до 18 лет просмотр запрещён")); + } + + /** + * Тестируем список всех рейтингов + */ + @Test + void findAllMpa() throws Exception { + MvcResult result = mvc.perform(get("/mpa")) + .andExpect(status().isOk()) + .andReturn(); + + List mpas = gson.fromJson(result.getResponse().getContentAsString(), + new MpaListTypeToken().getType()); + for (Mpa mpa : mpas) { + assertTrue(testMpaList.contains(mpa), + "Получен неизвестный рейтинг: " + mpa.toString()); + } + } + + /** + * Тестируем поиск рейтинга по идентификаторам + * + * @throws Exception + */ + @Test + void findMpaById() throws Exception { + for (Mpa mpa : testMpaList) { + MvcResult result = mvc.perform(get("/mpa/" + mpa.getId())) + .andExpect(status().isOk()) + .andReturn(); + Mpa mpaDb = gson.fromJson(result.getResponse().getContentAsString(), Mpa.class); + assertTrue(mpa.equals(mpaDb), + "Получен неизвестный рейтинг: " + mpaDb.toString()); + } + } + +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java index 67c49b1..2ff733b 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java @@ -2,27 +2,39 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import org.junit.jupiter.api.BeforeEach; +import com.google.gson.reflect.TypeToken; +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.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import ru.yandex.practicum.filmorate.model.LocalDateAdapter; +import org.springframework.test.web.servlet.MvcResult; import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.validator.LocalDateAdapter; import java.time.LocalDate; +import java.util.List; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** - * Тестируем контроллер запросов данных о пользователях + * Тестируем контроллер запросов работы с данными о пользователях + *

+ * Для успешного выполнения тестов, при инициализации базы данных + * должна быть подготовлена информация о четырех тестовых пользователях. + * Файл первоначальных данных ./src/test/resources/data.sql */ @SpringBootTest @AutoConfigureMockMvc +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) class UserControllerTest { @Autowired MockMvc mvc; @@ -32,13 +44,8 @@ class UserControllerTest { .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) .create(); - /** - * Перед каждым тестом удаляем всех пользователей - */ - @BeforeEach - void setUp() throws Exception { - mvc.perform(delete("/users")) - .andExpect(status().isOk()); + // Определяем тип сериализации списка + class UserListTypeToken extends TypeToken> { } /** @@ -46,11 +53,12 @@ void setUp() throws Exception { */ @Test void findAllUser() throws Exception { - makeUsers(3); - - mvc.perform(get("/users")) - .andExpect(status().isOk()) // ожидается код статус 200 - .andDo(print()); + MvcResult result = mvc.perform(get("/users")) + .andExpect(status().isOk()) // ожидается код статус 200 + .andReturn(); + List users = gson.fromJson(result.getResponse().getContentAsString(), new UserListTypeToken().getType()); + assertTrue(!users.isEmpty(), + "Список пользователей пуст."); } /** @@ -58,17 +66,33 @@ void findAllUser() throws Exception { */ @Test void addNewUser() throws Exception { - User user = new User("User1234@domain", - "user1234", "test user", - LocalDate.now().minusYears(22)); + User user = new User(); + user.setLogin("user1234"); + user.setName("testUserName"); + user.setBirthday(LocalDate.now().minusYears(22)); + + user.setEmail("User1234_domain@"); String jsonString = gson.toJson(user); + // При добавлении пользователя некорректным Email + // должен возвращаться статус 400 "BadRequest" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + user.setEmail("User1234@domain"); + jsonString = gson.toJson(user); // При успешном добавлении пользователя - // должен возвращаться статус 200 "Ok" - mvc.perform(post("/users") + // должен возвращаться статус 201 "Created" + MvcResult result = mvc.perform(post("/users") .content(jsonString) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); + .andExpect(status().isCreated()) + .andReturn(); + // Сохраняем созданного пользователя + User userDb = gson.fromJson(result.getResponse().getContentAsString(), User.class); + assertNotNull(userDb.getId(), + "При добавлении пользователя должен быть присвоен ненулевой идентификатор"); // Повторное добавление пользователя // должно возвращать статус 400 "BadRequest" @@ -83,22 +107,30 @@ void addNewUser() throws Exception { */ @Test void updateUser() throws Exception { - User user = new User("User1234@domain", - "user0000", "testing user", - LocalDate.now().minusYears(22)); + User user = new User(); + user.setEmail("UserUpdate@domain"); + user.setLogin("userUpdate"); + user.setName("testUpdateUserName"); + user.setBirthday(LocalDate.now().minusYears(22)); String jsonString = gson.toJson(user); - // Создаем тестового пользователя - mvc.perform(post("/users") + // При успешном добавлении пользователя + // должен возвращаться статус 201 "Created" + MvcResult result = mvc.perform(post("/users") .content(jsonString) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); + .andExpect(status().isCreated()) + .andReturn(); + // Сохраняем созданного пользователя + User userDb = gson.fromJson(result.getResponse().getContentAsString(), User.class); - user.setLogin("user12345"); + // готовим данные для обновления + user.setLogin("userUpd12345"); user.setName("Updated user."); - jsonString = gson.toJson(user); + user.setBirthday(LocalDate.now().minusYears(22)); - // Обновление записи без идентификатора + jsonString = gson.toJson(user); + // Обновление записи без идентификатора пользователя // должно возвращать статус 400 "BadRequest" mvc.perform(put("/users") .content(jsonString) @@ -114,170 +146,155 @@ void updateUser() throws Exception { .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNotFound()); - user.setId(1); + user.setId(userDb.getId()); jsonString = gson.toJson(user); // Успешное обновление записи // должно возвращать статус 200 "Ok" - mvc.perform(put("/users") + result = mvc.perform(put("/users") .content(jsonString) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andReturn(); + // Сохраняем пользователя из ответа после обновления + userDb = gson.fromJson(result.getResponse().getContentAsString(), User.class); + + assertThat(userDb) + .usingRecursiveComparison() + .isEqualTo(user); } /** - * Тестируем добавление друзей + * Тестирование добавления "друга" * * @throws Exception */ @Test void addFriends() throws Exception { - makeUsers(3); - // Объявление в "друзья" несуществующего пользователя // должно возвращать статус 404 "NotFound" mvc.perform(put("/users/1000/friends/1") - .content("") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNotFound()); // Объявление в "друзья" несуществующего друга // должно возвращать статус 404 "NotFound()" mvc.perform(put("/users/1/friends/1000") - .content("") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNotFound()); // Объявление в "друзья" сущществующих пользователей // должно возвращать статус 200 "ok" mvc.perform(put("/users/1/friends/2") - .content("") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - - // Объявление в "друзья" сущществующих пользователей (граничный случай) - // должно возвращать статус 200 "ok" - mvc.perform(put("/users/3/friends/2") - .content("") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); } /** - * Тестируем удаление друзей + * Тестирование удаления пользователя из друзей * * @throws Exception */ @Test - void removeFriends() throws Exception { - addFriends(); + void breakUpFriends() throws Exception { + // Добавление в "друзья" пользователея + // должно возвращать статус 200 "ok" + mvc.perform(put("/users/1/friends/2") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); // Удаление из "друзьей" не сущществующих пользователей // должно возвращать статус 404 "NotFound" mvc.perform(delete("/users/1/friends/1000") - .content("") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNotFound()); // Удаление из "друзьей" сущществующих пользователей // должно возвращать статус 200 "Ok" mvc.perform(delete("/users/1/friends/2") - .content("") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); } /** - * Тестируем чтение списка друзей + * Тестирование поиска друзей пользователя * * @throws Exception */ @Test - void getFriends() throws Exception { - makeUsers(3); - - // Объявление в "друзья" + void findUsersFriends() throws Exception { + // Добавление в "друзья" пользователея // должно возвращать статус 200 "ok" - mvc.perform(put("/users/1/friends/2") - .content("") + mvc.perform(put("/users/4/friends/2") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); - // Объявление в "друзья" + // Добавление в "друзья" пользователея // должно возвращать статус 200 "ok" - mvc.perform(put("/users/3/friends/2") - .content("") + mvc.perform(put("/users/4/friends/3") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); // читаем список "друзей", несуществующего пользователя // должно возвращать статус 404 "NotFound" mvc.perform(get("/users/2000/friends") - .content("") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNotFound()); - // читаем список "друзей" // должно возвращать статус 200 "ok" - mvc.perform(get("/users/2/friends") - .content("") + MvcResult result = mvc.perform(get("/users/4/friends") .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andReturn(); + List users = gson.fromJson(result.getResponse().getContentAsString(), new UserListTypeToken().getType()); + assertTrue(users.size() == 2, + "Количество найденых \"друзей\" не соответствует ожидаемому."); } /** - * Тестируем поиск общих друзей + * Тестирование поиска общих друзей у пользователей * * @throws Exception */ @Test - void findCommonFrends() throws Exception { - makeUsers(3); - - // Объявление в "друзья" - // должно возвращать статус 200 "ok" + void findCommonFriends() throws Exception { mvc.perform(put("/users/1/friends/2") - .content("") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); - // Объявление в "друзья" - // должно возвращать статус 200 "ok" + mvc.perform(put("/users/1/friends/3") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + mvc.perform(put("/users/1/friends/4") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + mvc.perform(put("/users/3/friends/2") - .content("") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + mvc.perform(put("/users/3/friends/1") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + mvc.perform(put("/users/4/friends/2") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + mvc.perform(put("/users/4/friends/3") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); // читаем список общих "друзей" // должно возвращать статус 200 "ok" - mvc.perform(get("/users/1/friends/common/3") - .content("") + MvcResult result = mvc.perform(get("/users/1/friends/common/4") .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andReturn(); + List users = gson.fromJson(result.getResponse().getContentAsString(), new UserListTypeToken().getType()); + assertTrue(users.size() == 2, + "Количество общих \"друзей\" не соответствует ожидаемому."); } - /** - * Создание тестовых пользователей - * - * @param count - требуемое клличество тестовых пользователей - * @throws Exception - */ - void makeUsers(int count) throws Exception { - StringBuilder fBuilder = new StringBuilder(); - fBuilder.append("{\"email\": \"user000%d@domain\","); - fBuilder.append("\"login\": \"USER000%d\","); - fBuilder.append("\"name\": \"userName00%d\","); - fBuilder.append("\"birthday\": \"2000-01-%02d\"}"); - String formatStr = fBuilder.toString(); - - for (int i = 1; i <= count; i++) { - String jsonString = String.format(formatStr, i, i, i, i); - // При успешном добавлении пользователя - // должен возвращаться статус 200 "Ok" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); - } - } -} +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/model/FilmApiTest.java b/src/test/java/ru/yandex/practicum/filmorate/model/FilmApiTest.java deleted file mode 100644 index a72aa0e..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/model/FilmApiTest.java +++ /dev/null @@ -1,136 +0,0 @@ -package ru.yandex.practicum.filmorate.model; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - -import java.time.LocalDate; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -/** - * Тестирование ограничений на значения полей класса Film при Http запросах - * Тестирование использования объектов в качестве параметров методов - */ -@SpringBootTest -@AutoConfigureMockMvc -class FilmApiTest { - @Autowired - private MockMvc mvc; - - private Gson gson = new GsonBuilder() - .setPrettyPrinting() - .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) - .create(); - - /** - * Перед каждым тестом очищаем список фильмов. - */ - @BeforeEach - void setUp() throws Exception { - mvc.perform(delete("/films")) - .andExpect(status().isOk()); - } - - /** - * Проверка непустого названия фильма. - */ - @Test - void testName() throws Exception { - Film film = new Film("", - "Testing film.name", - LocalDate.now().minusYears(10), - 60, 0); - String jsonString = gson.toJson(film); - // При добавлении фильма без названия - // должен возвращаться статус 400 "BadRequest" - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andDo(print()); - } - - /** - * Проверка допусимого размера описания. - */ - @Test - void testDescription() throws Exception { - Film film = new Film("Film", - "12345678901234567890123456789012345678901234567890" - + "12345678901234567890123456789012345678901234567890" - + "12345678901234567890123456789012345678901234567890" - + "12345678901234567890123456789012345678901234567890" - + "12345678901234567890123456789012345678901234567890", - LocalDate.now().minusYears(10), - 60, 0); - String jsonString = gson.toJson(film); - // При добавлении фильма - // должен возвращаться статус 400 "BadRequest" - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andDo(print()); - - } - - /** - * Проверка допустимой даты выпуска фильма - */ - @Test - void testReleaseDate() throws Exception { - Film film = new Film("Film", - "Testing film.releaseDate", - LocalDate.now().plusDays(1), - 60, 0); - String jsonString = gson.toJson(film); - // При добавлении фильма - // должен возвращаться статус 400 "BadRequest" - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andDo(print()); - - film.setReleaseDate(LocalDate.of(1895, 12, 27)); - jsonString = gson.toJson(film); - // При добавлении фильма - // должен возвращаться статус 400 "BadRequest" - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andDo(print()); - } - - /** - * Проверка допустимой длительности фильма - */ - @Test - void testDuration() throws Exception { - Film film = new Film("Film", - "Testing film.releaseDate", - LocalDate.now().minusYears(10), - 0, 0); - String jsonString = gson.toJson(film); - - // При добавлении фильма - // должен возвращаться статус 400 "BadRequest" - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andDo(print()); - } - -} diff --git a/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java b/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java index f5da073..621ab84 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java @@ -1,3 +1,4 @@ + package ru.yandex.practicum.filmorate.model; import jakarta.validation.ConstraintViolation; @@ -6,6 +7,7 @@ import jakarta.validation.ValidatorFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import ru.yandex.practicum.filmorate.validator.Marker; import java.time.LocalDate; import java.util.Set; @@ -15,7 +17,6 @@ /** * Тестирование ограничений на значения полей класса Film - * Автономный тест (Junit). */ class FilmTest { private Validator validator; @@ -34,10 +35,12 @@ void setUp() { */ @Test void testName() { - Film film = new Film("", - "Testing film.name", - LocalDate.now().minusYears(10), - 60, 0); + Film film = new Film(); + film.setName(""); + film.setDescription("Testing film"); + film.setReleaseDate(LocalDate.now().minusYears(10)); + film.setDuration(60); + film.setMpa(new Mpa(1)); Set> violations = validator.validate(film, Marker.OnBasic.class); assertFalse(violations.isEmpty()); @@ -48,14 +51,16 @@ void testName() { */ @Test void testDescription() { - Film film = new Film("Film", - "12345678901234567890123456789012345678901234567890" - + "12345678901234567890123456789012345678901234567890" - + "12345678901234567890123456789012345678901234567890" - + "12345678901234567890123456789012345678901234567890" - + "12345678901234567890123456789012345678901234567890", - LocalDate.now().minusYears(10), - 60, 0); + Film film = new Film(); + film.setName("Film"); + film.setDescription("12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890"); + film.setReleaseDate(LocalDate.now().minusYears(10)); + film.setDuration(60); + film.setMpa(new Mpa(1)); Set> violations = validator.validate(film, Marker.OnBasic.class); assertFalse(violations.isEmpty()); @@ -66,14 +71,18 @@ void testDescription() { */ @Test void testReleaseDate() { - Film film = new Film("Film", - "Testing film.releaseDate", - LocalDate.now().plusDays(1), - 60, 0); - + Film film = new Film(); + film.setName("Film"); + film.setDescription("Testing film ReleaseDate"); + film.setReleaseDate(LocalDate.now().plusDays(1)); + film.setDuration(60); + film.setMpa(new Mpa(1)); + + // Проверяем на контроль даты в будущем Set> violations = validator.validate(film, Marker.OnBasic.class); assertFalse(violations.isEmpty()); + // Проверяем на контроль минимальной даты выхода фильма film.setReleaseDate(LocalDate.of(1895, 12, 27)); violations.clear(); @@ -86,10 +95,12 @@ void testReleaseDate() { */ @Test void testDuration() { - Film film = new Film("Film", - "Testing film.duration", - LocalDate.now().minusYears(10), - 0, 0); + Film film = new Film(); + film.setName("Film"); + film.setDescription("Testing film ReleaseDate"); + film.setReleaseDate(LocalDate.now().minusYears(10)); + film.setDuration(0); + film.setMpa(new Mpa(1)); Set> violations = validator.validate(film, Marker.OnBasic.class); assertFalse(violations.isEmpty()); @@ -100,13 +111,15 @@ void testDuration() { */ @Test void testFilmOk() { - Film film = new Film("Film Ok", - "Testing film", - LocalDate.now().minusYears(10), - 60, 0); + Film film = new Film(); + film.setName("Film"); + film.setDescription("Testing film ReleaseDate"); + film.setReleaseDate(LocalDate.now().minusYears(10)); + film.setDuration(60); + film.setMpa(new Mpa(1)); Set> violations = validator.validate(film, Marker.OnBasic.class); assertTrue(violations.isEmpty(), violations.toString()); } -} \ No newline at end of file +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/model/UserApiTest.java b/src/test/java/ru/yandex/practicum/filmorate/model/UserApiTest.java deleted file mode 100644 index f14854a..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/model/UserApiTest.java +++ /dev/null @@ -1,209 +0,0 @@ -package ru.yandex.practicum.filmorate.model; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - -import java.time.LocalDate; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -/** - * Тестирование ограничений на значения класса User при Http запросах. - * Тестирование использования объектов в качестве параметров методов - */ -@SpringBootTest -@AutoConfigureMockMvc -class UserApiTest { - @Autowired - private MockMvc mvc; - - private Gson gson = new GsonBuilder() - .setPrettyPrinting() - .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) - .create(); - - /** - * Перед каждым тестом удаляем всех пользователей - */ - @BeforeEach - void setUp(/*@Autowired MockMvc mvc*/) throws Exception { - mvc.perform(delete("/users")) - .andExpect(status().isOk()); - } - - /** - * Тестирование email пользователя - */ - @Test - void testEmail() throws Exception { - User user = new User(null, - "userTest", - "Testing user", - LocalDate.now().minusYears(32)); - String jsonString = gson.toJson(user); - - // Создание пользователя без email - // должно возвращать статус 400 "BadRequest" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - user.setEmail("user.domain@"); - jsonString = gson.toJson(user); - // Создание пользователя с неправильным email - // должно возвращать статус 400 "BadRequest" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - user.setEmail("user@domain"); - jsonString = gson.toJson(user); - // Создание пользователя с корректным email - // должно возвращать статус 201 "Created" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); - } - - /** - * Тестирование login пользователя - */ - @Test - void testLogin() throws Exception { - User user = new User("user1234@test", - "", - "Testing user", - LocalDate.now().minusYears(32)); - String jsonString = gson.toJson(user); - - // Создание пользователя с пустым login - // должно возвращать статус 400 "BadRequest" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - user.setLogin("user test"); - jsonString = gson.toJson(user); - // Создание пользователя с login содержащим пробел - // должно возвращать статус 400 "BadRequest" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - user.setLogin("user1234"); - jsonString = gson.toJson(user); - // Создание пользователя с корректным login (содержит только латинские буквы и цифры) - // должно возвращать статус 201 "Created" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); - } - - /** - * Тестируем корректность даты рождения - */ - @Test - void testBirthday() throws Exception { - User user = new User("user1234@test", - "user1234", - "Testing user", - LocalDate.now().plusDays(30)); - - String jsonString = gson.toJson(user); - // Создание пользователя с датой рождения в будущем - // должно возвращать статус 400 "BadRequest" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - user.setBirthday(LocalDate.now().minusYears(30)); - jsonString = gson.toJson(user); - // Создание тестового пользователя с корректной датой рождения - // должно возвращать статус 200 "Ok" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); - } - - /** - * Тестируем группу аннотаций для режима обновления данных - */ - @Test - void testUpdateUser() throws Exception { - User user = new User("user1234@test", - "user1234", - "Testing user", - LocalDate.now().minusYears(32)); - String jsonString = gson.toJson(user); - // Создание тестового пользователя - // должно возвращать статус 201 "Created" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); - - jsonString = "{\"id\": 1, \"email\": \"user.domain@\"}"; - // Изменение пользователю email на некорректный - // должно возвращать статус 400 "BadRequest" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - jsonString = "{\"id\": 1, \"email\": \"user@host.domain\"}"; - // Изменение пользователю email на допустимый - // должно возвращать статус 200 "Ok" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - - jsonString = "{\"id\": 1, \"login\": \"user test12\"}"; - // Изменение пользователю login на некорректный - // должно возвращать статус 400 "BadRequest" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - jsonString = "{\"id\": 1, \"login\": \"userTest\"}"; - // Изменение пользователю login на допустимый - // должно возвращать статус 200 "Ok" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - - jsonString = "{\"id\": 1, \"birthday\": \"2050-01-01\"}"; - // Обновление пользователя с датой рождения в будущем - // должно возвращать статус 400 "BadRequest" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - jsonString = "{\"id\": 1, \"birthday\": \"2005-01-01\"}"; - // Обновление пользователя корректной датой рождения - // должно возвращать статус 200 "Ok" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - } -} diff --git a/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java b/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java index 2e61eb5..43a2218 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java @@ -1,3 +1,4 @@ + package ru.yandex.practicum.filmorate.model; import jakarta.validation.ConstraintViolation; @@ -6,6 +7,7 @@ import jakarta.validation.ValidatorFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import ru.yandex.practicum.filmorate.validator.Marker; import java.time.LocalDate; import java.util.Set; @@ -15,10 +17,7 @@ /** * Тестирование ограничений на значения полей класса User. - * Автономный тест (Junit). */ -// @SpringBootTest -// @AutoConfigureMockMvc class UserTest { private Validator validator; diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorageTest.java new file mode 100644 index 0000000..362fc38 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorageTest.java @@ -0,0 +1,211 @@ +package ru.yandex.practicum.filmorate.storage.film; + +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.Film; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.time.LocalDate; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Тестирование Хранилища Фильмов в базе данных + *

+ * Для успешного выполнения тестов, при инициализации базы данных + * должна быть подготовлена информация о четырех тестовых фильмах. + * Для фильма с id=TEST_FILM_ID нужно создать запись в таблице жанров. + * Файл первоначальных данных ./src/test/resources/data.sql + */ +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@Import({FilmDbStorage.class}) +class FilmDbStorageTest { + public static final int TEST_FILM_ID = 1; + + private final FilmDbStorage filmDbStorage; + + /** + * Генерация информации о тестовом фильме + * Поля должны соответствовать содержимому базы данных для фильма TEST_FILM_ID + * + * @return - объект, который ожидается для TEST_FILM_ID + */ + static Film getTestFilm() { + Film film = new Film(); + film.setId(TEST_FILM_ID); + film.setName("TestFilmName"); + film.setDescription("TestFilmDescription"); + film.setReleaseDate(LocalDate.of(2001, 2, 3)); + film.setDuration(51); + film.setMpa(new Mpa(1)); + film.addGenre(new Genre(1, "Комедия")); + return film; + } + + /** + * Тестирование добавления информации о новом фильме + */ + @Test + void addNewFilm() { + Film film = new Film(); + film.setName("Фильм! Фильм! Фильм!"); + film.setDescription("Юмористический рссказ о том, как делают кино."); + film.setReleaseDate(LocalDate.of(1968, 9, 1)); + film.setDuration(20); + film.setMpa(new Mpa(1)); + film.addGenre(new Genre(1, "Комедия")); + film.addGenre(new Genre(3, "Мультфильм")); + + Film filmDb = filmDbStorage.addNewFilm(film); + assertNotNull(filmDb.getId(), + "При добавлении нового фильа должен быть присвоен ненулевой идентификатор."); + + // Сравниваем исходный фильм с сохраненным по всем полям + Optional filmOptional = filmDbStorage.getFilmById(filmDb.getId()); + assertThat(filmOptional) + .isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(filmDb); + } + + /** + * Тестирование чтения информации о фильме по заданному идентификатору + */ + @Test + void getFilmById() { + Film film = getTestFilm(); + Optional filmOptional; + + // Попытка чтения несушествующего фильма + filmOptional = filmDbStorage.getFilmById(10000); + assertThat(filmOptional) + .withFailMessage("При чтении несуществующего фильма должен возвращаться пустой объект") + .isEmpty(); + + filmOptional = filmDbStorage.getFilmById(film.getId()); + assertThat(filmOptional) + .isPresent() + .get() + .isEqualTo(film); + } + + /** + * Тестирование списка фильмов + */ + @Test + void findAllFilms() { + Collection films = filmDbStorage.findAllFilms(); + assertTrue(films.size() > 0, + "findAllFilms() - В базе данных отсутствует информация о фильмах."); + } + + /** + * Тестирование расчета популярности фильмов + */ + @Test + void findPopularFilms() { + // задаем "лайки" к фильмам + filmDbStorage.addNewLike(1, 1); + filmDbStorage.addNewLike(2, 1); + filmDbStorage.addNewLike(2, 2); + filmDbStorage.addNewLike(3, 1); + filmDbStorage.addNewLike(3, 2); + filmDbStorage.addNewLike(3, 3); + filmDbStorage.addNewLike(3, 4); + filmDbStorage.addNewLike(4, 4); + filmDbStorage.addNewLike(4, 2); + filmDbStorage.addNewLike(4, 3); + + Collection films = filmDbStorage.findPopularFilms(2); + List popular = new LinkedList<>(films); + assertEquals(popular.get(0), filmDbStorage.getFilmById(3).get(), + "Самый популярный фильм расчитан неверно."); + + assertEquals(popular.get(1), filmDbStorage.getFilmById(4).get(), + "Второй по популярности фильм расчитан неверно."); + } + + /** + * Тестирование обновления информации о фильме + */ + @Test + void updateFilm() { + Film film = getTestFilm(); + film.setName("filmNameUpdated"); + film.setReleaseDate(LocalDate.of(1999, 12, 31)); + film.addGenre(new Genre(6, "Боевик")); + + filmDbStorage.updateFilm(film); + + Optional filmOptional = filmDbStorage.getFilmById(film.getId()); + assertThat(filmOptional) + .isPresent() + .get() + .isEqualTo(film); + } + + /** + * Тестирование добавления "лайка" к фильму + */ + @Test + void addNewLike() { + Film film = getTestFilm(); + film.setId(null); + film.setName("TestLikeFilmName"); + film.setDescription("TestLikeFilmDescription"); + + Film filmDb = filmDbStorage.addNewFilm(film); + + Integer rank = filmDbStorage.addNewLike(filmDb.getId(), 1); + assertEquals(1, rank, "При добавлении \"лйка\" произошла ошибка."); + + rank = filmDbStorage.addNewLike(filmDb.getId(), 3); + assertEquals(2, rank, "При подсчете \"лйков\" произошла ошибка."); + + rank = filmDbStorage.addNewLike(filmDb.getId(), 3); + assertEquals(2, rank, "При добавлении повторного \"лйка\" произошла ошибка счетчика."); + } + + /** + * Тестирование удаления "лайка у фильма" + */ + @Test + void removeLike() { + final int userId = 1; + + Integer expectedRank = filmDbStorage.addNewLike(TEST_FILM_ID, userId) - 1; + Integer rank = filmDbStorage.removeLike(TEST_FILM_ID, userId); + + assertEquals(expectedRank, rank, + "При удалении \"лайка\" произошла ошибка."); + } + + /** + * Тестирование Удаления информации о всех фильмах + */ + @Test + void removeAllFilms() { + Film film = getTestFilm(); + film.setId(null); + film.setName("TestFilmNameForRmove"); + filmDbStorage.addNewFilm(film); + + filmDbStorage.removeAllFilms(); + Collection films = filmDbStorage.findAllFilms(); + assertTrue(films.size() == 0, + "При удалении фильмов произошла ошибка."); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorageTest.java new file mode 100644 index 0000000..27c146b --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorageTest.java @@ -0,0 +1,77 @@ +package ru.yandex.practicum.filmorate.storage.genre; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeAll; +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.Genre; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Тестирование справочника жанров фильа + *

+ * Для успешного выполнения тестов, при инициализации базы данных + * должндолжен быть полностью заполнен справочник жанров. + * Файл первоначальных данных ./src/test/resources/data.sql + */ +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@Import({GenreDbStorage.class}) +class GenreDbStorageTest { + + private final GenreDbStorage genreDbStorage; + private static final List testGenres = new ArrayList<>(); + + /** + * Инициализация эталонного списка жанров. + */ + @BeforeAll + static void setUp() { + testGenres.add(new Genre(1, "Комедия")); + testGenres.add(new Genre(2, "Драма")); + testGenres.add(new Genre(3, "Мультфильм")); + testGenres.add(new Genre(4, "Триллер")); + testGenres.add(new Genre(5, "Документальный")); + testGenres.add(new Genre(6, "Боевик")); + } + + /** + * Тестирование списка жанров + */ + @Test + void findAllGenres() { + Collection genres = genreDbStorage.findAllGenres(); + for (Genre genre : testGenres) { + assertTrue(genres.contains(genre), + "В базе данных отсутствует " + genre.toString()); + } + } + + /** + * Тестирование поиска жанров по идентификатору + */ + @Test + void findGenre() { + for (Genre genre : testGenres) { + Optional genreOptional = genreDbStorage.findGenre(genre.getId()); + + assertThat(genreOptional) + .isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(genre); + } + } + +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorageTest.java new file mode 100644 index 0000000..8fb0bc7 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorageTest.java @@ -0,0 +1,75 @@ +package ru.yandex.practicum.filmorate.storage.mpa; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeAll; +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 java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Тестирование справочника рейтингов фильа + *

+ * Для успешного выполнения тестов, при инициализации базы данных + * должндолжен быть полностью заполнен справочник рейтингов MPA. + * Файл первоначальных данных ./src/test/resources/data.sql + */ +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@Import({MpaDbStorage.class}) +class MpaDbStorageTest { + + private final MpaDbStorage mpaDbStorage; + private static List testMpaList = new ArrayList<>(); + + /** + * Инициализация эталонного списка рейтингов. + */ + @BeforeAll + static void setUp() { + testMpaList.add(new Mpa(1, "G", "у фильма нет возрастных ограничений")); + testMpaList.add(new Mpa(2, "PG", "детям рекомендуется смотреть фильм с родителями")); + testMpaList.add(new Mpa(3, "PG-13", "детям до 13 лет просмотр не желателен")); + testMpaList.add(new Mpa(4, "R", "лицам до 17 лет просматривать фильм можно только в присутствии взрослого")); + testMpaList.add(new Mpa(5, "NC-17", "лицам до 18 лет просмотр запрещён")); + } + + /** + * Тестирование списка рейтингов + */ + @Test + void findAllMpa() { + Collection genres = mpaDbStorage.findAllMpa(); + for (Mpa mpa : testMpaList) { + assertTrue(genres.contains(mpa), + "В базе данных отсутствует " + mpa.toString()); + } + } + + /** + * Тестирование поиска рейтинга по идентификатору + */ + @Test + void findMpaById() { + for (Mpa mpa : testMpaList) { + Optional mpaOptional = mpaDbStorage.findMpa(mpa.getId()); + assertThat(mpaOptional) + .isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(mpa); + } + } + +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java new file mode 100644 index 0000000..40632ae --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java @@ -0,0 +1,238 @@ +package ru.yandex.practicum.filmorate.storage.user; + +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.DuplicateKeyException; +import ru.yandex.practicum.filmorate.model.User; + +import java.time.LocalDate; +import java.util.Collection; +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Тестирование Хранилища пользователей в базе данных + *

+ * Для успешного выполнения тестов, при инициализации базы данных + * должна быть подготовлена информация о четырех тестовых пользователях. + * Файл первоначальных данных ./src/test/resources/data.sql + */ +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@Import({UserDbStorage.class}) +class UserDbStorageTest { + public static final int TEST_USER_ID = 1; + + private final UserDbStorage userDbStorage; + + /** + * Генерация тестового пользователя + * + * @return - Optional информация о пользователе если найден + */ + static User getTestUser() { + User user = new User(); + user.setId(TEST_USER_ID); + user.setEmail("test@test.com"); + user.setLogin("testLogin"); + user.setName("testName"); + user.setBirthday(LocalDate.of(2001, 9, 22)); + return user; + } + + /** + * Чтение информации о тестовом пользователе по заданному идентификатору. + */ + @Test + void getUserById() { + User user = getTestUser(); + Optional userOptional = userDbStorage.getUserById(1); + + assertThat(userOptional) + .isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(user); + } + + /** + * Тест добавления в базу данных нового пользователя + */ + @Test + void addNewUser() { + User user = new User(); + user.setName("TesstUserName"); + user.setLogin("TestUserLogin"); + user.setBirthday(LocalDate.of(2001, 7, 22)); + + // Проверяем попытку добавить пользвателя с неуникальным Email + user.setEmail(getTestUser().getEmail()); + assertThrows(DuplicateKeyException.class, + () -> { + userDbStorage.addNewUser(user); + }, + "Попытка записи неуникального значения Email должна приводить к исключению."); + + // меняем Email на уникальный + user.setEmail("test@user.test"); + User userDb = userDbStorage.addNewUser(user); + assertNotNull(userDb.getId(), + "addNewUser() - При добавлении пользователя в базу должен быть присвоен не нулевой идентификатор"); + + Optional userOptional = userDbStorage.getUserById(userDb.getId()); + assertThat(userOptional) + .isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(user); + } + + /** + * Тестирование поиска информации о всех пользователях в базе. + */ + @Test + void findAllUsers() { + Collection users = userDbStorage.findAllUsers(); + + assertTrue(users.size() > 0, + "findAllUsers() - В базе данных отсутствует информация о пользователях."); + } + + /** + * Тестирование обновления информации о тестовом пользователе. + */ + @Test + void updateUser() { + User userUpdate = getTestUser(); + userUpdate.setEmail("updated_user@test.com"); + + userDbStorage.updateUser(userUpdate); + + Optional userOptional = userDbStorage.getUserById(userUpdate.getId()); + + assertThat(userOptional) + .isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(userUpdate); + } + + /** + * Тестирование добавления "друга" + */ + @Test + void addFriend() { + final int userId = TEST_USER_ID; + final int friendId = 2; + + Optional userOptional = userDbStorage.getUserById(userId); + assertThat(userOptional) + .withFailMessage("addFriend() - Не определен пользователь id=%s для создания \"друзей\".", userId) + .isPresent(); + if (userOptional.isEmpty()) return; + + userOptional = userDbStorage.getUserById(friendId); + assertThat(userOptional) + .withFailMessage("addFriend() - Не определен пользователь id=%s в качестве нового \"друга\".", friendId) + .isPresent(); + if (userOptional.isEmpty()) return; + User friend = userOptional.get(); + + userDbStorage.addFriend(userId, friendId); + Collection friends = userDbStorage.getUserFriends(userId); + + assertTrue(friends.contains(friend), + "addFriend() - Пользователь id=" + userId + ". не удалось добавить друга id=" + friendId); + } + + /** + * Тестирование списка друзей заданного пользователя + */ + @Test + void getUserFriends() { + userDbStorage.addFriend(TEST_USER_ID, 2); + userDbStorage.addFriend(TEST_USER_ID, 3); + userDbStorage.addFriend(TEST_USER_ID, 4); + + Collection friends = userDbStorage.getUserFriends(TEST_USER_ID); + assertTrue(friends.size() > 0, + "getUserFriends() - Список друзй пользователя id=" + + TEST_USER_ID + " не найден."); + assertTrue(friends.size() == 3, + "getUserFriends() - Количество друзй пользователя id=" + + TEST_USER_ID + " не соответствует ожидаемому."); + + User testFriend = userDbStorage.getUserById(2).get(); + assertTrue(friends.contains(testFriend), + "getUserFriends() - В списке друзей не найден " + testFriend.toString()); + + testFriend = userDbStorage.getUserById(3).get(); + assertTrue(friends.contains(testFriend), + "getUserFriends() - В списке друзей не найден " + testFriend.toString()); + + testFriend = userDbStorage.getUserById(4).get(); + assertTrue(friends.contains(testFriend), + "getUserFriends() - В списке друзей не найден " + testFriend.toString()); + } + + /** + * Тестирование удаления пользователя из друзей + */ + @Test + void removeFriend() { + final int deletedFriendId = 3; + getUserFriends(); + + userDbStorage.breakUpFriends(TEST_USER_ID, deletedFriendId); + Collection friends = userDbStorage.getUserFriends(TEST_USER_ID); + + Optional friend = friends.stream() + .filter(user -> user.getId() == deletedFriendId) + .findFirst(); + + assertThat(friend) + .withFailMessage("removeFriend() - пользователь id=%s не был удален из \"друзей\"%s.", + deletedFriendId, TEST_USER_ID) + .isEmpty(); + } + + /** + * Тестирование поиска общих друзей + */ + @Test + void findCommonFriends() { + getUserFriends(); + userDbStorage.addFriend(3, 1); + userDbStorage.addFriend(3, 4); + + Collection friends = userDbStorage.getCommonFriends(TEST_USER_ID, 3); + Optional commonFriend = friends.stream().findFirst(); + + assertThat(commonFriend) + .isPresent() + .hasValueSatisfying(user -> + assertThat(user) + .hasFieldOrPropertyWithValue("id", 4)); + } + + /** + * Тестирование удаления пользователей + */ + @Test + void removeAllUsers() { + addNewUser(); + + userDbStorage.removeAllUsers(); + Collection users = userDbStorage.findAllUsers(); + + assertTrue(users.size() == 0, + "removeAllUsers() - Не удалось удалить всех пользователей."); + } +} \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..71a0274 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,8 @@ +spring.sql.init.mode=always +spring.datasource.url=jdbc:h2:file:./db/filmorate +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password + +server.port=8080 +logging.level.org.zalando.logbook: TRACE diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql new file mode 100644 index 0000000..b070a66 --- /dev/null +++ b/src/test/resources/data.sql @@ -0,0 +1,34 @@ +-- Заполняем справочник жанров +MERGE INTO genres (id, name) + VALUES ( 1, 'Комедия'), + (2, 'Драма'), + (3, 'Мультфильм'), + (4, 'Триллер'), + (5, 'Документальный'), + (6, 'Боевик'); + +-- Заполняем справочник рейтингов MPA +MERGE INTO MPA (id, name, description) + VALUES (1, 'G', 'у фильма нет возрастных ограничений'), + (2, 'PG', 'детям рекомендуется смотреть фильм с родителями'), + (3, 'PG-13', 'детям до 13 лет просмотр не желателен'), + (4, 'R', 'лицам до 17 лет просматривать фильм можно только в присутствии взрослого'), + (5, 'NC-17', 'лицам до 18 лет просмотр запрещён'); + +-- Создаем тестовоых пользователей +INSERT INTO users (email, login, name, birthday) +VALUES ( 'test@test.com', 'testLogin', 'testName', '2001-9-22' ), + ('user1@test.com', 'userTest1', 'userNane1', '2001-01-01'), + ('user2@test.com', 'userTest2', 'userNane2', '2002-02-02'), + ('user3@test.com', 'userTest3', 'userNane3', '2003-03-03'); + +-- Создаем тестовые фильмы +INSERT INTO films (name, description, releasedate, len_min, mpa_id) +VALUES ( 'TestFilmName', 'TestFilmDescription', '2001-02-03', 51, 1), + ( 'TestFilmName2', 'TestFilmDescription2', '2002-03-04', 62, 2), + ( 'TestFilmName3', 'TestFilmDescription3', '2003-04-05', 73, 3), + ( 'TestFilmName4', 'TestFilmDescription4', '2004-05-06', 92, 4); + +INSERT INTO films_genres (film_id, genre_id) +VALUES ( 1, 1 ); + diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql new file mode 100644 index 0000000..714f97c --- /dev/null +++ b/src/test/resources/schema.sql @@ -0,0 +1,53 @@ +-- Создаем таблицу пользователей +CREATE TABLE IF NOT EXISTS users ( + id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + login VARCHAR(40) NOT NULL, + name VARCHAR(40) NOT NULL, + birthday DATE NOT NULL + ); + +-- Создаем таблицу друзей +CREATE TABLE IF NOT EXISTS friends ( + user_id INTEGER NOT NULL REFERENCES users(id), + friend_id INTEGER NOT NULL REFERENCES users(id), + confirmed BOOLEAN NOT NULL DEFAULT FALSE, + PRIMARY KEY (user_id, friend_id) + ); + +-- Создаем справочник жанров фильма +CREATE TABLE IF NOT EXISTS genres ( + id INTEGER PRIMARY KEY, + name VARCHAR(40) NOT NULL + ); + +-- Создаем справочник рейтинга MPA +CREATE TABLE IF NOT EXISTS MPA ( + id INTEGER PRIMARY KEY, + name VARCHAR(8) NOT NULL, + description VARCHAR(80) + ); + +-- Создаем таблицу описания фильма +CREATE TABLE IF NOT EXISTS films ( + id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name VARCHAR(40) NOT NULL, + description VARCHAR(200), + releaseDate DATE, + len_min INTEGER, + MPA_id INTEGER NOT NULL REFERENCES MPA(id) + ); + +-- Создаем таблицу описания жанра фильма +CREATE TABLE IF NOT EXISTS films_genres ( + film_id INTEGER NOT NULL REFERENCES films(id), + genre_id INTEGER NOT NULL REFERENCES genres(id), + PRIMARY KEY (film_id, genre_id) + ); + +-- Создаем таблицу "лайков" к фильмам +CREATE TABLE IF NOT EXISTS likes ( + user_id INTEGER NOT NULL REFERENCES users(id), + film_id INTEGER NOT NULL REFERENCES films(id), + PRIMARY KEY (user_id, film_id) + );