From 8558a5fc2e6f62a27cade730f3861ebcfab0821f Mon Sep 17 00:00:00 2001 From: Daria Simonenko Date: Wed, 14 Jan 2026 15:33:52 +0300 Subject: [PATCH 1/2] =?UTF-8?q?=D0=A1=D0=BF=D1=80=D0=B8=D0=BD=D1=82=2012?= =?UTF-8?q?=20-=20=D0=9F=D1=80=D0=BE=D0=BC=D0=B5=D0=B6=D1=83=D1=82=D0=BE?= =?UTF-8?q?=D1=87=D0=BD=D0=BE=D0=B5=20=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5.=20=D0=A1=D1=85=D0=B5=D0=BC=D0=B0=20=D0=91=D0=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +++ database_scheme.png | Bin 0 -> 65290 bytes 2 files changed, 3 insertions(+) create mode 100644 database_scheme.png diff --git a/README.md b/README.md index 2cf454a..c1f5a5f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # java-filmorate Template repository for Filmorate project. + +### Database scheme +![database_scheme.png](database_scheme.png) \ No newline at end of file diff --git a/database_scheme.png b/database_scheme.png new file mode 100644 index 0000000000000000000000000000000000000000..fffc276682ea5431a3c52ae0117d44cca0ef337c GIT binary patch literal 65290 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 V?R7cvvM2b44AiMs&r`h|@ISuHLA3w? literal 0 HcmV?d00001 From da9819fb677bd6a42c83cf2cce1b72a5ef8b8c4c Mon Sep 17 00:00:00 2001 From: Daria Simonenko Date: Fri, 6 Feb 2026 22:35:51 +0300 Subject: [PATCH 2/2] =?UTF-8?q?=D0=A1=D0=BF=D1=80=D0=B8=D0=BD=D1=82=2012?= =?UTF-8?q?=20-=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20DAO-=D1=81=D0=BB=D0=BE=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 32 +- .../filmorate/controller/FilmController.java | 24 +- .../filmorate/controller/GenreController.java | 39 ++ .../filmorate/controller/MpaController.java | 36 ++ .../filmorate/controller/UserController.java | 22 +- .../practicum/filmorate/dto/GenreDto.java | 11 + .../practicum/filmorate/dto/MpaDto.java | 11 + .../practicum/filmorate/dto/film/FilmDto.java | 23 ++ .../filmorate/dto/film/NewFilmRequest.java | 24 ++ .../filmorate/dto/film/UpdateFilmRequest.java | 49 +++ .../filmorate/dto/user/NewUserRequest.java | 19 + .../filmorate/dto/user/UpdateUserRequest.java | 35 ++ .../practicum/filmorate/dto/user/UserDto.java | 16 + .../exception/InternalServerException.java | 7 + .../filmorate/mapper/FilmMapper.java | 73 ++++ .../filmorate/mapper/GenreMapper.java | 20 + .../practicum/filmorate/mapper/MpaMapper.java | 20 + .../filmorate/mapper/UserMapper.java | 57 +++ .../practicum/filmorate/model/Film.java | 2 + .../practicum/filmorate/model/Genre.java | 13 + .../yandex/practicum/filmorate/model/Mpa.java | 13 + .../filmorate/service/FilmService.java | 45 +-- .../filmorate/service/GenreService.java | 33 ++ .../filmorate/service/MpaService.java | 33 ++ .../filmorate/service/UserService.java | 55 +-- .../filmorate/storage/FilmStorage.java | 15 + .../filmorate/storage/GenreStorage.java | 16 + .../storage/InMemoryFilmStorage.java | 21 ++ .../storage/InMemoryUserStorage.java | 24 ++ .../filmorate/storage/MpaStorage.java | 17 + .../filmorate/storage/UserStorage.java | 17 + .../filmorate/storage/dao/BaseStorage.java | 65 ++++ .../filmorate/storage/dao/FilmDbStorage.java | 271 ++++++++++++++ .../filmorate/storage/dao/GenreDbStorage.java | 41 +++ .../filmorate/storage/dao/MpaDbStorage.java | 41 +++ .../filmorate/storage/dao/UserDbStorage.java | 154 ++++++++ .../storage/dao/mapper/FilmRowMapper.java | 29 ++ .../storage/dao/mapper/GenreRowMapper.java | 19 + .../storage/dao/mapper/MpaRowMapper.java | 19 + .../storage/dao/mapper/UserRowMapper.java | 33 ++ src/main/resources/application.properties | 9 +- src/main/resources/data.sql | 28 ++ src/main/resources/schema.sql | 49 +++ .../filmorate/FilmorateApplicationTests.java | 31 +- .../controller/FilmControllerTest.java | 232 +++++++----- .../controller/GenreControllerTest.java | 48 +++ .../controller/MpaControllerTest.java | 48 +++ .../controller/UserControllerTest.java | 239 +++++++----- .../storage/dao/FilmDbStorageTest.java | 346 ++++++++++++++++++ .../storage/dao/GenreDbStorageTest.java | 48 +++ .../storage/dao/MpaDbStorageTest.java | 48 +++ .../storage/dao/UserDbStorageTest.java | 186 ++++++++++ 52 files changed, 2537 insertions(+), 269 deletions(-) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/GenreDto.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/MpaDto.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/film/FilmDto.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/film/NewFilmRequest.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/film/UpdateFilmRequest.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/user/NewUserRequest.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/user/UpdateUserRequest.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/user/UserDto.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/exception/InternalServerException.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/mapper/GenreMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/mapper/MpaMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/mapper/UserMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/Genre.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/Mpa.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/GenreStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/MpaStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/dao/BaseStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/dao/FilmDbStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/dao/GenreDbStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/dao/MpaDbStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/dao/UserDbStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/dao/mapper/FilmRowMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/dao/mapper/GenreRowMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/dao/mapper/MpaRowMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/dao/mapper/UserRowMapper.java create mode 100644 src/main/resources/data.sql create mode 100644 src/main/resources/schema.sql create mode 100644 src/test/java/ru/yandex/practicum/filmorate/controller/GenreControllerTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/controller/MpaControllerTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/dao/FilmDbStorageTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/dao/GenreDbStorageTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/dao/MpaDbStorageTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/dao/UserDbStorageTest.java diff --git a/pom.xml b/pom.xml index 383688d..5bffe1a 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,6 @@ org.springframework.boot spring-boot-starter-web - org.projectlombok lombok @@ -42,15 +41,42 @@ logbook-spring-boot-starter 3.7.2 + + org.springframework.boot + spring-boot-starter-jdbc + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + runtime + - + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.3.1 + + checkstyle.xml + + + + + check + + compile + + + org.springframework.boot spring-boot-maven-plugin - 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 0e16103..4c0b29a 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -5,7 +5,9 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.dto.film.FilmDto; +import ru.yandex.practicum.filmorate.dto.film.NewFilmRequest; +import ru.yandex.practicum.filmorate.dto.film.UpdateFilmRequest; import ru.yandex.practicum.filmorate.service.FilmService; import java.util.List; @@ -23,7 +25,7 @@ public FilmController(FilmService filmService) { * Возвращает фильм по id */ @GetMapping("/{id}") - public ResponseEntity getFilmById(@PathVariable int id) { + public ResponseEntity getFilmById(@PathVariable int id) { return ResponseEntity.ok(filmService.getFilmById(id)); } @@ -31,32 +33,30 @@ public ResponseEntity getFilmById(@PathVariable int id) { * Возвращает все фильмы в виде списка */ @GetMapping - public ResponseEntity> getFilms() { + public ResponseEntity> getFilms() { return ResponseEntity.ok(filmService.getFilms()); } /** * Добавляет новый фильм. * - * @param newFilm объект фильма, который нужно добавить + * @param newFilmDto объект фильма, который нужно добавить * @return ответ, содержащий созданный фильм с присвоенным ID */ @PostMapping - public ResponseEntity addFilm(@Valid @RequestBody Film newFilm) { - Film createdFilm = filmService.addFilm(newFilm); - return ResponseEntity.status(HttpStatus.CREATED).body(createdFilm); + public ResponseEntity addFilm(@Valid @RequestBody NewFilmRequest newFilmDto) { + return ResponseEntity.status(HttpStatus.CREATED).body(filmService.addFilm(newFilmDto)); } /** * Обновляет фильм. * - * @param updatedFilm объект фильма, который нужно обновить + * @param updatedFilmDto объект фильма, который нужно обновить * @return ответ, содержащий обновлённый фильм */ @PutMapping - public ResponseEntity updateFilm(@Valid @RequestBody Film updatedFilm) { - Film savedFilm = filmService.updateFilm(updatedFilm); - return ResponseEntity.ok(savedFilm); + public ResponseEntity updateFilm(@Valid @RequestBody UpdateFilmRequest updatedFilmDto) { + return ResponseEntity.ok(filmService.updateFilm(updatedFilmDto)); } /** @@ -92,7 +92,7 @@ public ResponseEntity removeLikeByUser(@PathVariable int id, @PathVariable * @return список самых популярных фильмов */ @GetMapping("/popular") - public ResponseEntity> getMostPopularFilms( + public ResponseEntity> getMostPopularFilms( @RequestParam(required = false, defaultValue = "10") int count) { return ResponseEntity.ok(filmService.getMostPopularFilms(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..b7efcc3 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java @@ -0,0 +1,39 @@ +package ru.yandex.practicum.filmorate.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import ru.yandex.practicum.filmorate.dto.GenreDto; +import ru.yandex.practicum.filmorate.service.GenreService; + +import java.util.List; + +@RestController +@RequestMapping("/genres") +public class GenreController { + private final GenreService genreService; + + @Autowired + public GenreController(GenreService genreService) { + this.genreService = genreService; + } + + /** + * Возвращает жанр по id + */ + @GetMapping("/{id}") + public ResponseEntity getGenreById(@PathVariable int id) { + return ResponseEntity.ok(genreService.getGenreById(id)); + } + + /** + * Возвращает все жанры в виде списка + */ + @GetMapping + public ResponseEntity> getGenres() { + return ResponseEntity.ok(genreService.getGenres()); + } +} 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..d87736f --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java @@ -0,0 +1,36 @@ +package ru.yandex.practicum.filmorate.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.dto.MpaDto; +import ru.yandex.practicum.filmorate.service.MpaService; + +import java.util.List; + +@RestController +@RequestMapping("/mpa") +public class MpaController { + private final MpaService mpaService; + + @Autowired + public MpaController(MpaService mpaService) { + this.mpaService = mpaService; + } + + /** + * Возвращает рейтинг MPA по id + */ + @GetMapping("/{id}") + public ResponseEntity getMpaById(@PathVariable int id) { + return ResponseEntity.ok(mpaService.getMpaById(id)); + } + + /** + * Возвращает все рейтинги MPA + */ + @GetMapping + public ResponseEntity> getMpaList() { + return ResponseEntity.ok(mpaService.getMpaList()); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java index a441f3a..c4b41ae 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -5,7 +5,9 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.dto.user.NewUserRequest; +import ru.yandex.practicum.filmorate.dto.user.UpdateUserRequest; +import ru.yandex.practicum.filmorate.dto.user.UserDto; import ru.yandex.practicum.filmorate.service.UserService; import java.util.List; @@ -23,7 +25,7 @@ public UserController(UserService userService) { * Возвращает пользователя по id */ @GetMapping("/{id}") - public ResponseEntity getUserById(@PathVariable int id) { + public ResponseEntity getUserById(@PathVariable int id) { return ResponseEntity.ok(userService.getUserById(id)); } @@ -31,7 +33,7 @@ public ResponseEntity getUserById(@PathVariable int id) { * Возвращает всех пользователей в виде списка */ @GetMapping - public ResponseEntity> getUsers() { + public ResponseEntity> getUsers() { return ResponseEntity.ok(userService.getUsers()); } @@ -42,9 +44,8 @@ public ResponseEntity> getUsers() { * @return ответ, содержащий созданного пользователя с присвоенным ID */ @PostMapping - public ResponseEntity addUser(@Valid @RequestBody User newUser) { - User createdUser = userService.addUser(newUser); - return ResponseEntity.status(HttpStatus.CREATED).body(createdUser); + public ResponseEntity addUser(@Valid @RequestBody NewUserRequest newUser) { + return ResponseEntity.status(HttpStatus.CREATED).body(userService.addUser(newUser)); } /** @@ -54,9 +55,8 @@ public ResponseEntity addUser(@Valid @RequestBody User newUser) { * @return ответ, содержащий обновлённого пользователя */ @PutMapping - public ResponseEntity updateUser(@Valid @RequestBody User updatedUser) { - User savedUser = userService.updateUser(updatedUser); - return ResponseEntity.ok(savedUser); + public ResponseEntity updateUser(@Valid @RequestBody UpdateUserRequest updatedUser) { + return ResponseEntity.ok(userService.updateUser(updatedUser)); } /** @@ -92,7 +92,7 @@ public ResponseEntity removeFriend(@PathVariable int id, @PathVariable int * @return список друзей-пользователей указанного пользователя. */ @GetMapping("/{id}/friends") - public ResponseEntity> getAllFriends(@PathVariable int id) { + public ResponseEntity> getAllFriends(@PathVariable int id) { return ResponseEntity.ok(userService.getUserFriendsById(id)); } @@ -104,7 +104,7 @@ public ResponseEntity> getAllFriends(@PathVariable int id) { * @return список общих друзей в виде списка пользователей. */ @GetMapping("/{id}/friends/common/{otherId}") - public ResponseEntity> getCommonFriends(@PathVariable int id, @PathVariable int otherId) { + public ResponseEntity> getCommonFriends(@PathVariable int id, @PathVariable int otherId) { return ResponseEntity.ok(userService.getCommonFriends(id, otherId)); } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/GenreDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/GenreDto.java new file mode 100644 index 0000000..67b8ad9 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/GenreDto.java @@ -0,0 +1,11 @@ +package ru.yandex.practicum.filmorate.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class GenreDto { + private Integer id; + private String name; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/MpaDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/MpaDto.java new file mode 100644 index 0000000..d0faa73 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/MpaDto.java @@ -0,0 +1,11 @@ +package ru.yandex.practicum.filmorate.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class MpaDto { + private Integer id; + private String name; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/film/FilmDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/film/FilmDto.java new file mode 100644 index 0000000..16fa38b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/film/FilmDto.java @@ -0,0 +1,23 @@ +package ru.yandex.practicum.filmorate.dto.film; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.time.LocalDate; +import java.util.Set; + +@Data +@NoArgsConstructor +public class FilmDto { + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private int id; + private String name; + private String description; + private LocalDate releaseDate; + private Integer duration; + private Set genres; + private Mpa mpa; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/film/NewFilmRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/film/NewFilmRequest.java new file mode 100644 index 0000000..1b499a9 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/film/NewFilmRequest.java @@ -0,0 +1,24 @@ +package ru.yandex.practicum.filmorate.dto.film; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.time.LocalDate; +import java.util.Set; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class NewFilmRequest { + private String name; + private String description; + private LocalDate releaseDate; + private Integer duration; + private Set genres; + private Mpa mpa; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/film/UpdateFilmRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/film/UpdateFilmRequest.java new file mode 100644 index 0000000..da3f4e4 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/film/UpdateFilmRequest.java @@ -0,0 +1,49 @@ +package ru.yandex.practicum.filmorate.dto.film; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.time.LocalDate; +import java.util.Set; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UpdateFilmRequest { + private int id; + private String name; + private String description; + private LocalDate releaseDate; + private Integer duration; + private Set genres; + private Mpa mpa; + + public boolean hasName() { + return !(name == null || name.isBlank()); + } + + public boolean hasDescription() { + return !(description == null || description.isBlank()); + } + + public boolean hasReleaseDate() { + return !(releaseDate == null); + } + + public boolean hasDuration() { + return !(duration == null); + } + + public boolean hasGenres() { + return !(genres == null || genres.isEmpty()); + } + + public boolean hasMpaRating() { + return !(mpa == null); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/user/NewUserRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/user/NewUserRequest.java new file mode 100644 index 0000000..d360111 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/user/NewUserRequest.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.dto.user; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class NewUserRequest { + private String name; + private String email; + private String login; + private LocalDate birthday; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/user/UpdateUserRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/user/UpdateUserRequest.java new file mode 100644 index 0000000..d5080a5 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/user/UpdateUserRequest.java @@ -0,0 +1,35 @@ +package ru.yandex.practicum.filmorate.dto.user; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.time.LocalDate; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UpdateUserRequest { + int id; + private String name; + private String email; + private String login; + private LocalDate birthday; + + public boolean hasLogin() { + return ! (login == null || login.isBlank()); + } + + public boolean hasName() { + return ! (name == null || name.isBlank()); + } + + public boolean hasEmail() { + return ! (email == null || email.isBlank()); + } + + public boolean hasBirthday() { + return ! (birthday == null); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/user/UserDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/user/UserDto.java new file mode 100644 index 0000000..fdab327 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/user/UserDto.java @@ -0,0 +1,16 @@ +package ru.yandex.practicum.filmorate.dto.user; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Data +@NoArgsConstructor +public class UserDto { + private int id; + private String name; + private String email; + private String login; + private LocalDate birthday; +} 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..c9eba3a --- /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); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java new file mode 100644 index 0000000..dadcf11 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java @@ -0,0 +1,73 @@ +package ru.yandex.practicum.filmorate.mapper; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import ru.yandex.practicum.filmorate.dto.film.FilmDto; +import ru.yandex.practicum.filmorate.dto.film.NewFilmRequest; +import ru.yandex.practicum.filmorate.dto.film.UpdateFilmRequest; +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.Set; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class FilmMapper { + public static Film mapToFilm(NewFilmRequest request) { + String name = request.getName(); + String description = request.getDescription(); + LocalDate releaseDate = request.getReleaseDate(); + Integer duration = request.getDuration(); + Set genres = request.getGenres(); + Mpa mpaRatingId = request.getMpa(); + + return Film.builder() + .name(name) + .description(description) + .releaseDate(releaseDate) + .duration(duration) + .genres(genres) + .mpa(mpaRatingId) + .build(); + } + + public static FilmDto mapToFilmDto(Film film) { + FilmDto dto = new FilmDto(); + dto.setId(film.getId()); + dto.setName(film.getName()); + dto.setDescription(film.getDescription()); + dto.setReleaseDate(film.getReleaseDate()); + dto.setDuration(film.getDuration()); + dto.setGenres(film.getGenres()); + dto.setMpa(film.getMpa()); + return dto; + } + + public static Film updateFilmFields(Film film, UpdateFilmRequest request) { + if (request.hasName()) { + film.setName(request.getName()); + } + + if (request.hasDescription()) { + film.setDescription(request.getDescription()); + } + + if (request.hasDuration()) { + film.setDuration(request.getDuration()); + } + + if (request.hasReleaseDate()) { + film.setReleaseDate(request.getReleaseDate()); + } + + if (request.hasGenres()) { + film.setGenres(request.getGenres()); + } + + if (request.hasMpaRating()) { + film.setMpa(request.getMpa()); + } + return film; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/GenreMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/GenreMapper.java new file mode 100644 index 0000000..2bd0ca5 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/GenreMapper.java @@ -0,0 +1,20 @@ +package ru.yandex.practicum.filmorate.mapper; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import ru.yandex.practicum.filmorate.dto.GenreDto; +import ru.yandex.practicum.filmorate.model.Genre; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class GenreMapper { + public static Genre mapToGenre(GenreDto request) { + return new Genre(request.getId(), request.getName()); + } + + public static GenreDto mapToGenreDto(Genre genre) { + GenreDto dto = new GenreDto(); + dto.setId(genre.getId()); + dto.setName(genre.getName()); + return dto; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/MpaMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/MpaMapper.java new file mode 100644 index 0000000..193a41f --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/MpaMapper.java @@ -0,0 +1,20 @@ +package ru.yandex.practicum.filmorate.mapper; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import ru.yandex.practicum.filmorate.dto.MpaDto; +import ru.yandex.practicum.filmorate.model.Mpa; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MpaMapper { + public static Mpa mapToMpa(MpaDto request) { + return new Mpa(request.getId(), request.getName()); + } + + public static MpaDto mapToMpaDto(Mpa mpa) { + MpaDto dto = new MpaDto(); + dto.setId(mpa.getId()); + dto.setName(mpa.getName()); + return dto; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/UserMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/UserMapper.java new file mode 100644 index 0000000..3d5ec7f --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/UserMapper.java @@ -0,0 +1,57 @@ +package ru.yandex.practicum.filmorate.mapper; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import ru.yandex.practicum.filmorate.dto.user.NewUserRequest; +import ru.yandex.practicum.filmorate.dto.user.UpdateUserRequest; +import ru.yandex.practicum.filmorate.dto.user.UserDto; +import ru.yandex.practicum.filmorate.model.User; + +import java.time.LocalDate; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class UserMapper { + public static User mapToUser(NewUserRequest request) { + String name = request.getName(); + String email = request.getEmail(); + String login = request.getLogin(); + LocalDate birthday = request.getBirthday(); + + return User.builder() + .name(name) + .email(email) + .login(login) + .birthday(birthday) + .build(); + } + + public static UserDto mapToUserDto(User user) { + UserDto dto = new UserDto(); + dto.setId(user.getId()); + dto.setName(user.getName()); + dto.setEmail(user.getEmail()); + dto.setLogin(user.getLogin()); + dto.setBirthday(user.getBirthday()); + return dto; + } + + public static User updateUserFields(User user, UpdateUserRequest request) { + if (request.hasLogin()) { + user.setLogin(request.getLogin()); + } + + if (request.hasName()) { + user.setName(request.getName()); + } + + if (request.hasEmail()) { + user.setEmail(request.getEmail()); + } + + if (request.hasBirthday()) { + user.setBirthday(request.getBirthday()); + } + + return user; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java index e9b9a17..dc918a2 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -27,4 +27,6 @@ public class Film { private Integer duration; private Set likes; + private Set genres; + private Mpa mpa; } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java b/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java new file mode 100644 index 0000000..137f4b0 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.*; + +@Data +@Builder(toBuilder = true) +@EqualsAndHashCode(of = { "id" }) +@AllArgsConstructor +@NoArgsConstructor +public class Genre { + private Integer 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..04b9dd9 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Mpa.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.*; + +@Data +@Builder(toBuilder = true) +@EqualsAndHashCode(of = { "id" }) +@AllArgsConstructor +@NoArgsConstructor +public class Mpa { + private Integer id; + private String name; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java index 5fd80e8..4a77c66 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -5,14 +5,17 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dto.film.FilmDto; +import ru.yandex.practicum.filmorate.dto.film.NewFilmRequest; +import ru.yandex.practicum.filmorate.dto.film.UpdateFilmRequest; import ru.yandex.practicum.filmorate.exception.ValidationException; +import ru.yandex.practicum.filmorate.mapper.FilmMapper; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.storage.FilmStorage; import ru.yandex.practicum.filmorate.storage.UserStorage; import java.time.LocalDate; import java.time.Month; -import java.util.Comparator; import java.util.List; @Service @@ -33,16 +36,16 @@ public FilmService(FilmStorage filmStorage, UserStorage userStorage) { * Возвращает фильм по id. * Вызывает метод хранилища по получению фильма по id. */ - public Film getFilmById(int id) { - return filmStorage.getFilmById(id); + public FilmDto getFilmById(int id) { + return FilmMapper.mapToFilmDto(filmStorage.getFilmById(id)); } /** * Возвращает все фильмы в виде списка. * Вызывает метод хранилища по получению всех фильмов */ - public List getFilms() { - return filmStorage.getFilms(); + public List getFilms() { + return filmStorage.getFilms().stream().map(FilmMapper::mapToFilmDto).toList(); } /** @@ -50,7 +53,8 @@ public List getFilms() { * Проверяет поля переданного фильма на соответствие. * Если всё хорошо, то вызывает соответствующий метод хранилища. */ - public Film addFilm(Film newFilm) { + public FilmDto addFilm(NewFilmRequest newFilmDto) { + Film newFilm = FilmMapper.mapToFilm(newFilmDto); try { checkIsValidFilm(newFilm); } catch (ValidationException exception) { @@ -58,7 +62,7 @@ public Film addFilm(Film newFilm) { throw exception; } log.trace("Данные фильма {} прошли валидацию при добавлении", newFilm.getId()); - return filmStorage.addFilm(newFilm); + return FilmMapper.mapToFilmDto(filmStorage.addFilm(newFilm)); } /** @@ -66,7 +70,8 @@ public Film addFilm(Film newFilm) { * Проверяет поля переданного фильма на соответствие. * Если всё хорошо, то вызывает соответствующий метод хранилища. */ - public Film updateFilm(Film updatedFilm) { + public FilmDto updateFilm(UpdateFilmRequest updatedFilmDto) { + Film updatedFilm = FilmMapper.updateFilmFields(filmStorage.getFilmById(updatedFilmDto.getId()), updatedFilmDto); try { checkIsValidFilm(updatedFilm); } catch (ValidationException exception) { @@ -75,7 +80,7 @@ public Film updateFilm(Film updatedFilm) { throw exception; } log.trace("Фильм {} прошёл валидацию для обновления", updatedFilm.getId()); - return filmStorage.updateFilm(updatedFilm); + return FilmMapper.mapToFilmDto(filmStorage.updateFilm(updatedFilm)); } /** @@ -86,13 +91,11 @@ public Film updateFilm(Film updatedFilm) { public void addLike(int filmId, int userId) { userStorage.getUserById(userId); log.trace("Пользователь {} найден для добавления лайка", userId); - Film film = filmStorage.getFilmById(filmId); + filmStorage.getFilmById(filmId); log.trace("Фильм {} найден для добавления лайка", filmId); - film.getLikes().add(userId); + filmStorage.addLike(filmId, userId); log.info("Добавление лайка от пользователя {} для фильма {} выполнено", userId, filmId); - - filmStorage.updateFilm(film); } /** @@ -106,22 +109,16 @@ public void removeLike(int filmId, int userId) { Film film = filmStorage.getFilmById(filmId); log.trace("Фильм {} найден для удаления лайка", filmId); - film.getLikes().remove(userId); + filmStorage.removeLike(filmId, userId); log.info("Удаление лайка от пользователя {} для фильма {} выполнено", userId, filmId); - - filmStorage.updateFilm(film); } /** * Возвращает список самых популярных фильмов в виде списка. * Вызывает метод хранилища по получению всех фильмов, сортирует их и фильтрует по количеству. */ - public List getMostPopularFilms(int count) { - return filmStorage.getFilms().stream() - .sorted(Comparator.comparingInt( - (Film film) -> film.getLikes().size()).reversed().thenComparingInt(Film::getId)) - .limit(count) - .toList(); + public List getMostPopularFilms(int count) { + return filmStorage.getMostPopularFilms(count).stream().map(FilmMapper::mapToFilmDto).toList(); } /** @@ -133,6 +130,10 @@ private void checkIsValidFilm(Film film) throws ValidationException { throw new ValidationException("Фильм для валидации входных параметров не найден"); } + if (film.getName().isBlank()) { + throw new ValidationException("Название фильма не может быть пустым"); + } + if (film.getDescription().length() > FILM_DESCRIPTION_LENGTH) { throw new ValidationException("Описание фильма не может быть больше 200 символов"); } 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..6c0e748 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java @@ -0,0 +1,33 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dto.GenreDto; +import ru.yandex.practicum.filmorate.mapper.GenreMapper; +import ru.yandex.practicum.filmorate.storage.GenreStorage; + +import java.util.List; + +@Service +public class GenreService { + private final GenreStorage genreStorage; + + @Autowired + public GenreService(GenreStorage genreStorage) { + this.genreStorage = genreStorage; + } + + /** + * Возвращает жанр по id. + */ + public GenreDto getGenreById(int id) { + return GenreMapper.mapToGenreDto(genreStorage.getGenreById(id)); + } + + /** + * Возвращает всех жанры в виде списка. + */ + public List getGenres() { + return genreStorage.getGenres().stream().map(GenreMapper::mapToGenreDto).toList(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java b/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java new file mode 100644 index 0000000..4d41d41 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java @@ -0,0 +1,33 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dto.MpaDto; +import ru.yandex.practicum.filmorate.mapper.MpaMapper; +import ru.yandex.practicum.filmorate.storage.MpaStorage; + +import java.util.List; + +@Service +public class MpaService { + private final MpaStorage mpaStorage; + + @Autowired + public MpaService(MpaStorage mpaStorage) { + this.mpaStorage = mpaStorage; + } + + /** + * Возвращает MPA рейтинг по id. + */ + public MpaDto getMpaById(int id) { + return MpaMapper.mapToMpaDto(mpaStorage.getMpaById(id)); + } + + /** + * Возвращает всех MPA рейтинги в виде списка. + */ + public List getMpaList() { + return mpaStorage.getMpaList().stream().map(MpaMapper::mapToMpaDto).toList(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java index 5ef5d21..b7b3a60 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -4,8 +4,12 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dto.user.NewUserRequest; +import ru.yandex.practicum.filmorate.dto.user.UpdateUserRequest; +import ru.yandex.practicum.filmorate.dto.user.UserDto; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; +import ru.yandex.practicum.filmorate.mapper.UserMapper; import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.storage.UserStorage; @@ -26,16 +30,16 @@ public UserService(UserStorage userStorage) { * Возвращает пользователя по id. * Вызывает метод хранилища по получению пользователя по id. */ - public User getUserById(int id) { - return userStorage.getUserById(id); + public UserDto getUserById(int id) { + return UserMapper.mapToUserDto(userStorage.getUserById(id)); } /** * Возвращает всех пользователей в виде списка. * Вызывает соответствующий метод хранилища. */ - public List getUsers() { - return userStorage.getUsers(); + public List getUsers() { + return userStorage.getUsers().stream().map(UserMapper::mapToUserDto).toList(); } /** @@ -46,7 +50,8 @@ public List getUsers() { * @param newUser объект пользователя, который нужно добавить * @return созданный пользователь с присвоенным ID */ - public User addUser(User newUser) { + public UserDto addUser(NewUserRequest newUserDto) { + User newUser = UserMapper.mapToUser(newUserDto); try { checkIsValidUser(newUser); } catch (ValidationException exception) { @@ -55,7 +60,7 @@ public User addUser(User newUser) { throw exception; } log.trace("Данные новго пользователя c логином {} прошли валидацию при добавлении", newUser.getLogin()); - return userStorage.addUser(newUser); + return UserMapper.mapToUserDto(userStorage.addUser(newUser)); } /** @@ -66,7 +71,10 @@ public User addUser(User newUser) { * @param updatedUser объект пользователя, который нужно обновить * @return обновлённый пользователь */ - public User updateUser(User updatedUser) { + public UserDto updateUser(UpdateUserRequest updatedUserDto) { + User updatedUser = userStorage.getUserById(updatedUserDto.getId()); + updatedUser = UserMapper.updateUserFields(updatedUser, updatedUserDto); + try { checkIsValidUser(updatedUser); } catch (ValidationException exception) { @@ -76,7 +84,7 @@ public User updateUser(User updatedUser) { } log.info("Данные пользователя {} прошли валидацию при обновлении", updatedUser.getId()); - return userStorage.updateUser(updatedUser); + return UserMapper.mapToUserDto(userStorage.updateUser(updatedUser)); } /** @@ -89,13 +97,11 @@ public void addFriends(int firstUserId, int secondUserId) { User secondUser = userStorage.getUserById(secondUserId); log.trace("Пользователь {} найден для добавления в друзья", secondUserId); - firstUser.getFriends().add(secondUserId); - secondUser.getFriends().add(firstUser.getId()); - log.info("Пользователь {} добавлен в друзья пользователя {}.", - secondUserId, firstUserId); + if (firstUser.getFriends().contains(secondUserId)) { + throw new ValidationException("Пользователь уже добавлен в друзья"); + } - userStorage.updateUser(firstUser); - userStorage.updateUser(secondUser); + userStorage.addFriends(firstUserId, secondUserId); log.trace("Пользователи обновлены в хранилище."); } @@ -109,13 +115,7 @@ public void removeFriends(int firstUserId, int secondUserId) { User secondUser = userStorage.getUserById(secondUserId); log.trace("Пользователь {} найден для удаления мз друзей", secondUserId); - firstUser.getFriends().remove(secondUserId); - secondUser.getFriends().remove(firstUserId); - log.info("Пользователь {} удален из друзей пользователя {}.", - secondUserId, firstUserId); - - userStorage.updateUser(firstUser); - userStorage.updateUser(secondUser); + userStorage.removeFriends(firstUserId, secondUserId); log.trace("Пользователи обновлены в хранилище."); } @@ -123,9 +123,9 @@ public void removeFriends(int firstUserId, int secondUserId) { * Ищет общих друзей у пользователей * Если оба пользователя существуют, то возвращает список общих друзей. */ - public List getCommonFriends(int firstUserId, int secondUserId) { - List firstFriends = getUserFriendsById(firstUserId); - List secondFriends = getUserFriendsById(secondUserId); + public List getCommonFriends(int firstUserId, int secondUserId) { + List firstFriends = getUserFriendsById(firstUserId); + List secondFriends = getUserFriendsById(secondUserId); log.trace("Списки друзей пользователей для поиска общих друзей составлены", secondUserId); return firstFriends.stream() @@ -137,11 +137,14 @@ public List getCommonFriends(int firstUserId, int secondUserId) { * Возвращает всех друзей пользователя * Если пользователь существуюет, то возвращает список по id */ - public List getUserFriendsById(int userId) { + public List getUserFriendsById(int userId) { User user = userStorage.getUserById(userId); log.trace("Пользователь {} найден для поиска всех его друзей", userId); - return user.getFriends().stream().map(id -> userStorage.getUserById(id)).toList(); + return user.getFriends().stream() + .map(id -> userStorage.getUserById(id)) + .map(UserMapper::mapToUserDto) + .toList(); } /** diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java index 8ba57f5..7517b35 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java @@ -23,4 +23,19 @@ public interface FilmStorage { * Обновляет фильм. */ Film updateFilm(Film updatedFilm); + + /** + * Добавляет лайк. + */ + void addLike(int filmId, int userId); + + /** + * Удаляет лайк. + */ + void removeLike(int filmId, int userId); + + /** + * Возвращает популярные фильмы + */ + List getMostPopularFilms(int count); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/GenreStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/GenreStorage.java new file mode 100644 index 0000000..d776e8b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/GenreStorage.java @@ -0,0 +1,16 @@ +package ru.yandex.practicum.filmorate.storage; + +import ru.yandex.practicum.filmorate.model.Genre; +import java.util.List; + +public interface GenreStorage { + /** + * Возвращает жанр по id + */ + Genre getGenreById(int id); + + /** + * Возвращает все жанры в виде списка + */ + List getGenres(); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryFilmStorage.java index e128d75..38c1f37 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryFilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryFilmStorage.java @@ -107,4 +107,25 @@ private int getNextId() { .orElse(0); return ++currentMaxId; } + + /** + * Добавляет лайк. + */ + public void addLike(int filmId, int userId) { + throw new NotFoundException("Метод не поддерживается"); + } + + /** + * Удаляет лайк. + */ + public void removeLike(int filmId, int userId) { + throw new NotFoundException("Метод не поддерживается"); + } + + /** + * Возвращает популярные фильмы + */ + public List getMostPopularFilms(int count) { + throw new NotFoundException("Метод не поддерживается"); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryUserStorage.java index 1af6df2..56104a3 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryUserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryUserStorage.java @@ -105,4 +105,28 @@ private int getNextId() { .orElse(0); return ++currentMaxId; } + + /** + * Заглушка для метода добавления в друзья + */ + @Override + public void addFriends(int userId, int friendId) { + throw new NotFoundException("Метод не поддерживается"); + } + + /** + * Заглушка для метода удаления из друзей + */ + @Override + public void removeFriends(int userId, int friendId) { + throw new NotFoundException("Метод не поддерживается"); + } + + /** + * Заглушка для метода получения друзей пользователя в виде списка id + */ + @Override + public Set getUserFriends(Integer userId) { + throw new NotFoundException("Метод не поддерживается"); + } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/MpaStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/MpaStorage.java new file mode 100644 index 0000000..ee7b2f9 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/MpaStorage.java @@ -0,0 +1,17 @@ +package ru.yandex.practicum.filmorate.storage; + +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.util.List; + +public interface MpaStorage { + /** + * Возвращает MPA рейтинг по id + */ + Mpa getMpaById(int id); + + /** + * Возвращает все MPA рейтинги в виде списка + */ + List getMpaList(); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java index 7a01079..f40c5b6 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java @@ -1,7 +1,9 @@ package ru.yandex.practicum.filmorate.storage; import ru.yandex.practicum.filmorate.model.User; + import java.util.List; +import java.util.Set; public interface UserStorage { /** @@ -33,4 +35,19 @@ public interface UserStorage { * @return копия обновлённого пользователя */ User updateUser(User updatedUser); + + /** + * Добавляет пользователя в друзья + */ + void addFriends(int userId, int friendId); + + /** + * Удаляет пользователя из друзей + */ + void removeFriends(int userId, int friendId); + + /** + * Возвращает друзей пользователя в виде списка id + */ + Set getUserFriends(Integer userId); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/dao/BaseStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/dao/BaseStorage.java new file mode 100644 index 0000000..1a3fa1c --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/dao/BaseStorage.java @@ -0,0 +1,65 @@ +package ru.yandex.practicum.filmorate.storage.dao; + +import lombok.RequiredArgsConstructor; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import ru.yandex.practicum.filmorate.exception.InternalServerException; + +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +public class BaseStorage { + protected final JdbcTemplate jdbc; + protected final RowMapper mapper; + + protected Optional findOne(String query, Object... params) { + try { + T result = jdbc.queryForObject(query, mapper, params); + return Optional.ofNullable(result); + } catch (EmptyResultDataAccessException ignored) { + return Optional.empty(); + } + } + + protected List findMany(String query, Object... params) { + return jdbc.query(query, mapper, params); + } + + protected boolean delete(String query, int id) { + int rowsDeleted = jdbc.update(query, id); + return rowsDeleted > 0; + } + + protected void update(String query, Object... params) { + int rowsUpdated = jdbc.update(query, params); + if (rowsUpdated == 0) { + throw new InternalServerException("Не удалось обновить данные"); + } + } + + protected int insert(String query, Object... params) { + GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); + jdbc.update(connection -> { + PreparedStatement ps = connection + .prepareStatement(query, Statement.RETURN_GENERATED_KEYS); + for (int idx = 0; idx < params.length; idx++) { + ps.setObject(idx + 1, params[idx]); + } + return ps; + }, keyHolder); + + Integer id = keyHolder.getKeyAs(Integer.class); + + // Возвращаем id нового пользователя + if (id != null) { + return id; + } else { + throw new InternalServerException("Не удалось сохранить данные"); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/dao/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/dao/FilmDbStorage.java new file mode 100644 index 0000000..eab7ab1 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/dao/FilmDbStorage.java @@ -0,0 +1,271 @@ +package ru.yandex.practicum.filmorate.storage.dao; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.exception.InternalServerException; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Mpa; +import ru.yandex.practicum.filmorate.storage.FilmStorage; +import ru.yandex.practicum.filmorate.storage.GenreStorage; +import ru.yandex.practicum.filmorate.storage.MpaStorage; +import ru.yandex.practicum.filmorate.storage.dao.mapper.FilmRowMapper; + +import java.util.*; + +@Primary +@Repository +public class FilmDbStorage extends BaseStorage implements FilmStorage { + private final MpaStorage mpaStorage; + private final GenreStorage genreStorage; + + private static final Logger log = LoggerFactory.getLogger(FilmDbStorage.class); + + private static final String GET_FILM_BY_ID = "SELECT * FROM films WHERE id = ?;"; + private static final String GET_FILMS = "SELECT * FROM films;"; + private static final String ADD_FILM = "INSERT INTO films (name, description, duration, release_date, " + + "mpa_rating_id) VALUES (?, ?, ?, ?, ?);"; + private static final String ADD_FILM_SIMPLE = "INSERT INTO films (name, description, duration, release_date) " + + "VALUES (?, ?, ?, ?);"; + private static final String UPDATE_FILM = "UPDATE films SET name = ?,description = ?, duration = ?, " + + "release_date = ?, mpa_rating_id = ? WHERE id = ?;"; + private static final String UPDATE_FILM_SHORT = "UPDATE films SET name = ?,description = ?, duration = ?, " + + "release_date = ? WHERE id = ?;"; + private static final String GET_POPULAR_FILMS = "SELECT f.*, COUNT(fl.user_id) as likes_count FROM films f " + + "LEFT JOIN films_likes fl ON f.id = fl.film_id " + + "GROUP BY f.id " + + "ORDER BY likes_count DESC " + + "LIMIT ?;"; + + private static final String GET_GENRES_ID = "SELECT genre_id FROM films_genres WHERE film_id = ?;"; + private static final String GET_GENRE_NAME = "SELECT name FROM genres WHERE id = ?;"; + private static final String ADD_GENRE = "INSERT INTO films_genres (film_id, genre_id) " + + "VALUES (?, ?);"; + private static final String DELETE_GENRES = "DELETE FROM films_genres WHERE film_id = ?;"; + + private static final String GET_MPA_ID = "SELECT mpa_rating_id FROM films WHERE id = ?;"; + private static final String GET_MPA_NAME = "SELECT name FROM mpa_ratings WHERE id = ?;"; + + private static final String ADD_USER_LIKE = "INSERT INTO films_likes (film_id, user_id) VALUES (?, ?);"; + private static final String DELETE_USER_LIKE = "DELETE FROM films_likes WHERE film_id = ? " + + "AND user_id = ?;"; + + + public FilmDbStorage(JdbcTemplate jdbc, FilmRowMapper mapper, MpaStorage mpaStorage, GenreStorage genreStorage) { + super(jdbc, mapper); + this.mpaStorage = mpaStorage; + this.genreStorage = genreStorage; + } + + /** + * Возвращает фильм по id из БД + */ + @Override + public Film getFilmById(int id) { + Film film = findOne(GET_FILM_BY_ID, id).orElseThrow(() -> { + String filmNotFound = "Фильм с id = " + id + " не найден"; + log.error(filmNotFound); + return new NotFoundException(filmNotFound); + }); + enrichFilmByAdditionalInfo(film); + return film; + } + + /** + * Возвращает все фильмы из БД в виде списка + */ + @Override + public List getFilms() { + return findMany(GET_FILMS).stream().peek(this::enrichFilmByAdditionalInfo).toList(); + } + + /** + * Добавляет новый фильм в БД + * + * @param newFilm объект фильма, который нужно добавить + * @return созданный фильм с присвоенным ID + */ + @Override + public Film addFilm(Film newFilm) { + if (newFilm == null) { + String filmNotFound = "Фильм для добавления не найден"; + log.error(filmNotFound); + throw new NotFoundException(filmNotFound); + } + checkFilmMpaValid(newFilm); + checkFilmGenresValid(newFilm); + + int id; + if (newFilm.getMpa() == null) { + id = insert(ADD_FILM_SIMPLE, newFilm.getName(), newFilm.getDescription(), newFilm.getDuration(), + newFilm.getReleaseDate()); + } else { + id = insert(ADD_FILM, newFilm.getName(), newFilm.getDescription(), newFilm.getDuration(), + newFilm.getReleaseDate(), newFilm.getMpa().getId()); + } + newFilm.setId(id); + + log.info("Фильму {} присвоен id {}", newFilm.getName(), newFilm.getId()); + if (newFilm.getGenres() != null && !newFilm.getGenres().isEmpty()) { + newFilm.getGenres().forEach(genre -> insert(ADD_GENRE, newFilm.getId(), genre.getId())); + } + + enrichFilmByAdditionalInfo(newFilm); + return newFilm; + } + + /** + * Обновляет фильм. + * Проверяет поля переданного фильма на соответствие. + * Если всё хорошо и переданный фильм существует в контроллере, то создаёт копию и сохраняет в контроллер. + * + * @param updatedFilm объект фильма, который нужно обновить + * @return копия обновлённого фильма + */ + @Override + public Film updateFilm(Film updatedFilm) { + try { + checkFilmMpaValid(updatedFilm); + checkFilmGenresValid(updatedFilm); + + if (updatedFilm.getMpa() == null) { + update(UPDATE_FILM_SHORT, updatedFilm.getName(), updatedFilm.getDescription(), updatedFilm.getDuration(), + updatedFilm.getReleaseDate(), updatedFilm.getId()); + } else { + update(UPDATE_FILM, updatedFilm.getName(), updatedFilm.getDescription(), updatedFilm.getDuration(), + updatedFilm.getReleaseDate(), updatedFilm.getMpa().getId(), updatedFilm.getId()); + } + + if (updatedFilm.getGenres() != null && !updatedFilm.getGenres().isEmpty()) { + update(DELETE_GENRES, updatedFilm.getId()); + updatedFilm.getGenres().forEach(genre -> + update(ADD_GENRE, updatedFilm.getId(), genre.getId())); + } + enrichFilmByAdditionalInfo(updatedFilm); + log.info("Фильм {} обновлён в хранилище", updatedFilm.getId()); + return updatedFilm; + } catch (InternalServerException exception) { + log.error("Фильм {} не обновлён из-за внутренней ошибки", updatedFilm.getId()); + throw exception; + } + } + + /** + * Добавляет лайк от пользователя. + * Делает запись в БД. + */ + public void addLike(int filmId, int userId) { + try { + update(ADD_USER_LIKE, filmId, userId); + } catch (InternalServerException exception) { + log.error("Не удалось добавить лайк для фильма {} от пользователя {}", filmId, userId); + throw exception; + } + } + + /** + * Удаляет лайк от пользователя. + * Делает запись в БД. + */ + public void removeLike(int filmId, int userId) { + try { + update(DELETE_USER_LIKE, filmId, userId); + } catch (InternalServerException exception) { + log.error("Не удалось удалить лайк для фильма {} от пользователя {}", filmId, userId); + throw exception; + } + } + + /** + * Возвращает ТОП фильмов по числу лайков + */ + public List getMostPopularFilms(int count) { + return findMany(GET_POPULAR_FILMS, count).stream().peek(this::enrichFilmByAdditionalInfo).toList(); + } + + /** + * Обогащает фильм дополнительной информацией. + */ + private Film enrichFilmByAdditionalInfo(Film film) { + if (film == null) { + String filmNotFound = "Фильм для обогащения не найден"; + log.error(filmNotFound); + throw new NotFoundException(filmNotFound); + } + film.setGenres(new LinkedHashSet<>(getFilmGenres(film.getId()))); + film.setMpa(getFilmMpaRating(film.getId())); + + if (film.getLikes() == null) { + film.setLikes(new HashSet<>()); + } + + return film; + } + + /** + * Формирует список жанров фильма + */ + private List getFilmGenres(int filmId) { + List genreIdList = jdbc.queryForList(GET_GENRES_ID, Integer.class, filmId); + + if (genreIdList.isEmpty()) { + return new ArrayList<>(); + } + + return genreIdList.stream() + .map(genreId -> { + Genre genre = new Genre(); + genre.setId(genreId); + return genre; + }).peek(genre -> { + String genreName = jdbc.queryForObject(GET_GENRE_NAME, String.class, genre.getId()); + genre.setName(genreName); + }) + .sorted(Comparator.comparing(Genre::getId)) + .toList(); + } + + /** + * Формирует объект MPA рейтинга + */ + private Mpa getFilmMpaRating(int filmId) { + Integer mpaRatingId = jdbc.queryForObject(GET_MPA_ID, Integer.class, filmId); + if (mpaRatingId == null) { + return null; + } + String mpaName = jdbc.queryForObject(GET_MPA_NAME, String.class, mpaRatingId); + return new Mpa(mpaRatingId, mpaName); + } + + /** + * Проверяет, что MPA рейтинг фильма существует + */ + private void checkFilmMpaValid(Film film) { + Mpa filmMpa = film.getMpa(); + if (filmMpa == null) { + return; + } + if (!mpaStorage.getMpaList().contains(filmMpa)) { + throw new NotFoundException("MPA рейтинг " + filmMpa.getId() + " не найден"); + } + } + + /** + * Проверяет, что каждый жанр фильма существует + */ + private void checkFilmGenresValid(Film film) { + Set filmGenres = film.getGenres(); + if (filmGenres == null || filmGenres.isEmpty()) { + return; + } + filmGenres.forEach(genre -> { + if (!genreStorage.getGenres().contains(genre)) { + throw new NotFoundException("MPA рейтинг " + genre.getId() + " не найден"); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/dao/GenreDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/dao/GenreDbStorage.java new file mode 100644 index 0000000..0bd4ab1 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/dao/GenreDbStorage.java @@ -0,0 +1,41 @@ +package ru.yandex.practicum.filmorate.storage.dao; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.storage.GenreStorage; +import ru.yandex.practicum.filmorate.storage.dao.mapper.GenreRowMapper; + +import java.util.List; + +@Repository +public class GenreDbStorage extends BaseStorage implements GenreStorage { + private static final Logger log = LoggerFactory.getLogger(GenreDbStorage.class); + private static final String GET_GENRE_BY_ID = "SELECT * FROM genres WHERE id = ?;"; + private static final String GET_ALL_GENRES = "SELECT * FROM genres ORDER BY id;"; + + public GenreDbStorage(JdbcTemplate jdbc, GenreRowMapper mapper) { + super(jdbc, mapper); + } + + /** + * Возвращает жанр по id + */ + public Genre getGenreById(int id) { + return findOne(GET_GENRE_BY_ID, id).orElseThrow(() -> { + String genreNotFound = "Жанр с id = " + id + " не найден"; + log.error(genreNotFound); + return new NotFoundException(genreNotFound); + }); + } + + /** + * Возвращает все жанры в виде списка + */ + public List getGenres() { + return findMany(GET_ALL_GENRES); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/dao/MpaDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/dao/MpaDbStorage.java new file mode 100644 index 0000000..56abb51 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/dao/MpaDbStorage.java @@ -0,0 +1,41 @@ +package ru.yandex.practicum.filmorate.storage.dao; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Mpa; +import ru.yandex.practicum.filmorate.storage.MpaStorage; +import ru.yandex.practicum.filmorate.storage.dao.mapper.MpaRowMapper; + +import java.util.List; + +@Repository +public class MpaDbStorage extends BaseStorage implements MpaStorage { + private static final Logger log = LoggerFactory.getLogger(MpaDbStorage.class); + private static final String GET_MPA_BY_ID = "SELECT * FROM mpa_ratings WHERE id = ?;"; + private static final String GET_ALL_MPA = "SELECT * FROM mpa_ratings ORDER BY id;"; + + public MpaDbStorage(JdbcTemplate jdbc, MpaRowMapper mapper) { + super(jdbc, mapper); + } + + /** + * Возвращает MPA рейтинг по id + */ + public Mpa getMpaById(int id) { + return findOne(GET_MPA_BY_ID, id).orElseThrow(() -> { + String mpaNotFound = "Фильм с id = " + id + " не найден"; + log.error(mpaNotFound); + return new NotFoundException(mpaNotFound); + }); + } + + /** + * Возвращает все MPA рейтинги в виде списка + */ + public List getMpaList() { + return findMany(GET_ALL_MPA); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/dao/UserDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/dao/UserDbStorage.java new file mode 100644 index 0000000..6130631 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/dao/UserDbStorage.java @@ -0,0 +1,154 @@ +package ru.yandex.practicum.filmorate.storage.dao; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.exception.InternalServerException; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.UserStorage; +import ru.yandex.practicum.filmorate.storage.dao.mapper.UserRowMapper; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Primary +@Repository("userDbStorage") +public class UserDbStorage extends BaseStorage implements UserStorage { + private static final Logger log = LoggerFactory.getLogger(UserDbStorage.class); + + private static final String GET_USER_BY_ID_QUERY = "SELECT * FROM users WHERE id = ?;"; + private static final String GET_USERS_QUERY = "SELECT * FROM users;"; + private static final String ADD_USER_QUERY = "INSERT INTO users (name, email, login, birthday)" + + "VALUES (?, ?, ?, ?);"; + private static final String UPDATE_USER_QUERY = "UPDATE users SET name = ?, email = ?, login = ?, birthday = ? " + + "WHERE id = ?;"; + private static final String GET_USER_FRIENDS = "SELECT friend_id FROM users_friendship WHERE user_id = ?;"; + private static final String ADD_USER_FRIEND = "INSERT INTO users_friendship (user_id, friend_id) VALUES (?, ?);"; + private static final String DELETE_USER_FRIEND = "DELETE FROM users_friendship WHERE user_id = ? " + + "AND friend_id = ?;"; + + @Autowired + public UserDbStorage(JdbcTemplate jdbc, UserRowMapper mapper) { + super(jdbc, mapper); + } + + /** + * Возвращает пользователя по id из БД + */ + @Override + public User getUserById(int id) { + User user = findOne(GET_USER_BY_ID_QUERY, id).orElseThrow(() -> { + String userNotFound = "Пользователь с id = " + id + " не найден"; + log.error(userNotFound); + return new NotFoundException(userNotFound); + }); + user.setFriends(getUserFriends(id)); + return user; + } + + /** + * Возвращает из БД всех пользователей в виде списка + */ + @Override + public List getUsers() { + try { + return findMany(GET_USERS_QUERY); + } catch (InternalServerException exception) { + log.error("Пользователи не найдены из-за внутренней ошибки"); + throw exception; + } + } + + /** + * Добавляет нового пользователя. + * Присваивает уникальный ID и сохраняет в БД. + * + * @param newUser объект пользователя, который нужно добавить + * @return созданный пользователь с присвоенным ID + */ + @Override + public User addUser(User newUser) { + int id = insert(ADD_USER_QUERY, newUser.getName(), newUser.getEmail(), newUser.getLogin(), + newUser.getBirthday()); + newUser.setId(id); + log.info("Пользователю с логином {} присвоен id {}", newUser.getLogin(), newUser.getId()); + + if (newUser.getFriends() == null) { + newUser.setFriends(new HashSet<>()); + } + + log.info("Пользователь {} добавлен в базу", newUser.getId()); + return newUser; + } + + /** + * Обновляет пользователя в БД, если он существует. + * + * @param updatedUser объект пользователя, который нужно обновить + * @return обновлённый пользователь + */ + @Override + public User updateUser(User updatedUser) { + try { + update(UPDATE_USER_QUERY, updatedUser.getName(), updatedUser.getEmail(), updatedUser.getLogin(), + updatedUser.getBirthday(), updatedUser.getId()); + + if (updatedUser.getFriends() == null) { + updatedUser.setFriends(new HashSet<>()); + } + + log.info("Пользователь {} обновлён", updatedUser.getId()); + return updatedUser; + } catch (InternalServerException exception) { + log.error("Пользователь {} не обновлён из-за внутренней ошибки", updatedUser.getId()); + throw exception; + } + } + + /** + * Добавляет в друзья пользователя другому пользователю. + * Делает запись в БД. + */ + @Override + public void addFriends(int userId, int friendId) { + try { + jdbc.update(ADD_USER_FRIEND, userId, friendId); + } catch (InternalServerException exception) { + log.error("Не удалось добавить пользователя {} в друзья {}", friendId, userId); + throw exception; + } + } + + /** + * Удаляет одного пользователя из друзей второго пользователя. + * Делает запись в БД. + */ + @Override + public void removeFriends(int userId, int friendId) { + try { + jdbc.update(DELETE_USER_FRIEND, userId, friendId); + } catch (InternalServerException exception) { + log.error("Не удалось удалить пользователя {} из друзей {}", friendId, userId); + throw exception; + } + } + + /** + * Возвращает список друзей пользователя в виде списка id + */ + @Override + public Set getUserFriends(Integer userId) { + try { + return new HashSet<>(jdbc.queryForList(GET_USER_FRIENDS, Integer.class, userId)); + } catch (InternalServerException exception) { + String getFriendsError = "Не удалось получить друзей пользователя " + userId; + log.error(getFriendsError); + throw exception; + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/dao/mapper/FilmRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/storage/dao/mapper/FilmRowMapper.java new file mode 100644 index 0000000..6529d3e --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/dao/mapper/FilmRowMapper.java @@ -0,0 +1,29 @@ +package ru.yandex.practicum.filmorate.storage.dao.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; +import java.time.LocalDate; + +@Component +public class FilmRowMapper implements RowMapper { + @Override + public Film mapRow(ResultSet resultSet, int rowNum) throws SQLException { + int id = resultSet.getInt("id"); + String name = resultSet.getString("name"); + String description = resultSet.getString("description"); + int duration = resultSet.getInt("duration"); + LocalDate releaseDate = resultSet.getDate("release_date").toLocalDate(); + + return Film.builder() + .id(id) + .name(name) + .description(description) + .duration(duration) + .releaseDate(releaseDate) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/dao/mapper/GenreRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/storage/dao/mapper/GenreRowMapper.java new file mode 100644 index 0000000..e8257b2 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/dao/mapper/GenreRowMapper.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.storage.dao.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/storage/dao/mapper/MpaRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/storage/dao/mapper/MpaRowMapper.java new file mode 100644 index 0000000..c84987c --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/dao/mapper/MpaRowMapper.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.storage.dao.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 { + @Override + public Mpa mapRow(ResultSet resultSet, int rowNum) throws SQLException { + Mpa mpa = new Mpa(); + mpa.setId(resultSet.getInt("id")); + mpa.setName(resultSet.getString("name")); + return mpa; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/dao/mapper/UserRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/storage/dao/mapper/UserRowMapper.java new file mode 100644 index 0000000..99e93ab --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/dao/mapper/UserRowMapper.java @@ -0,0 +1,33 @@ +package ru.yandex.practicum.filmorate.storage.dao.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; +import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; + +@Component +public class UserRowMapper implements RowMapper { + @Override + public User mapRow(ResultSet resultSet, int rowNum) throws SQLException { + int id = resultSet.getInt("id"); + String name = resultSet.getString("name"); + String email = resultSet.getString("email"); + String login = resultSet.getString("login"); + LocalDate birthday = resultSet.getDate("birthday").toLocalDate(); + Set friends = new HashSet<>(); + + return User.builder() + .id(id) + .name(name) + .email(email) + .login(login) + .birthday(birthday) + .friends(friends) + .build(); + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3eb70b5..bb52638 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,2 +1,7 @@ -logging.level.org.zalando.logbook: TRACE - +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 +sql.init.mode=always +h2.console.enabled=true \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 0000000..7bca888 --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,28 @@ +DELETE FROM users_friendship; +DELETE FROM films_likes; +DELETE FROM films_genres; +DELETE FROM films; +DELETE FROM users; +DELETE FROM genres; +DELETE FROM mpa_ratings; + +ALTER TABLE mpa_ratings ALTER COLUMN id RESTART WITH 1; +INSERT INTO mpa_ratings (name) VALUES +('G'), ('PG'), ('PG-13'), ('R'), ('NC-17'); + +ALTER TABLE genres ALTER COLUMN id RESTART WITH 1; +INSERT INTO genres (name) VALUES +('Комедия'), ('Драма'), ('Мультфильм'), ('Триллер'), ('Документальный'), ('Боевик'); + +ALTER TABLE users ALTER COLUMN id RESTART WITH 1; +INSERT INTO users (name, email, login, birthday) VALUES +('Иван Иванов', 'ivan@mail.com', 'ivan_ivanov', '1990-05-15'), +('Мария Петрова', 'maria@mail.com', 'maria_pet', '1992-08-22'), +('Алексей Сидоров', 'alex@mail.com', 'alex_sid', '1988-11-30'), +('Елена Сидорова', 'elena@mail.com', 'elena_sid', '1988-11-30'); + +ALTER TABLE films ALTER COLUMN id RESTART WITH 1; +ALTER TABLE users_friendship ALTER COLUMN id RESTART WITH 1; +ALTER TABLE films_likes ALTER COLUMN id RESTART WITH 1; +ALTER TABLE films_genres ALTER COLUMN id RESTART WITH 1; + diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..91cce65 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,49 @@ + +CREATE TABLE IF NOT EXISTS users ( + id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(255), + email VARCHAR(255) NOT NULL, + login VARCHAR(255) NOT NULL, + birthday DATE, + CONSTRAINT uq_user_email UNIQUE (email), + CONSTRAINT uq_user_login UNIQUE (login) +); + +CREATE TABLE IF NOT EXISTS genres ( + id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(100) NOT NULL +); + +CREATE TABLE IF NOT EXISTS mpa_ratings ( + id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(50) NOT NULL +); + +CREATE TABLE IF NOT EXISTS films ( + id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description VARCHAR(1000), + duration INTEGER NOT NULL CHECK (duration > 0), + release_date DATE NOT NULL, + mpa_rating_id INTEGER REFERENCES mpa_ratings(id) ON DELETE SET NULL, + CONSTRAINT release_date_chk CHECK (release_date >= '1895-12-28') +); + +CREATE TABLE IF NOT EXISTS films_genres ( + id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + film_id INTEGER NOT NULL REFERENCES films(id) ON DELETE CASCADE, + genre_id INTEGER NOT NULL REFERENCES genres(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS films_likes ( + id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + film_id INTEGER NOT NULL REFERENCES films(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS users_friendship ( + id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + friend_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + CONSTRAINT no_self_friendship_chk CHECK (user_id != friend_id) +); \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java b/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java index 660412e..a2ce107 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java +++ b/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java @@ -1,13 +1,34 @@ package ru.yandex.practicum.filmorate; +import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; +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.ComponentScan; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.dao.UserDbStorage; -@SpringBootTest +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@ComponentScan(basePackages = "ru.yandex.practicum.filmorate") class FilmorateApplicationTests { + private final UserDbStorage userStorage; @Test - void contextLoads() { - } + public void testFindUserById() { -} + Optional userOptional = Optional.of(userStorage.getUserById(1)); + + assertThat(userOptional) + .isPresent() + .hasValueSatisfying(user -> + assertThat(user).hasFieldOrPropertyWithValue("id", 1) + ); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java index 4a321e2..b0850cd 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java @@ -1,27 +1,34 @@ package ru.yandex.practicum.filmorate.controller; +import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; 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.ComponentScan; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.jdbc.core.JdbcTemplate; +import ru.yandex.practicum.filmorate.dto.film.FilmDto; +import ru.yandex.practicum.filmorate.dto.film.NewFilmRequest; +import ru.yandex.practicum.filmorate.dto.film.UpdateFilmRequest; +import ru.yandex.practicum.filmorate.dto.user.NewUserRequest; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; -import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.model.User; -import ru.yandex.practicum.filmorate.service.FilmService; -import ru.yandex.practicum.filmorate.service.UserService; -import ru.yandex.practicum.filmorate.storage.FilmStorage; -import ru.yandex.practicum.filmorate.storage.InMemoryFilmStorage; -import ru.yandex.practicum.filmorate.storage.InMemoryUserStorage; -import ru.yandex.practicum.filmorate.storage.UserStorage; import java.time.LocalDate; import java.time.Month; import java.util.List; - +import java.util.Objects; import static org.junit.jupiter.api.Assertions.*; + +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@ComponentScan(basePackages = "ru.yandex.practicum.filmorate") class FilmControllerTest { private static final String FILM_NAME = "Фильм"; private static final String FILM_NAME_2 = "Второй фильм"; @@ -44,38 +51,41 @@ class FilmControllerTest { private static final String USER_NAME = "Вася"; private static final String USER_EMAIL = "mail@mail.ru"; + private static final String USER_EMAIL_2 = "mail2@mail.ru"; private static final String USER_LOGIN = "user"; + private static final String USER_LOGIN_2 = "user2"; private static final LocalDate USER_BIRTHDAY = LocalDate.of(1992, Month.DECEMBER, 12); private static final Integer USER_ID = 567; - FilmController filmController; - UserController userController; + private static final String DELETE_FILMS = "DELETE FROM films;"; + private static final String DELETE_USERS = "DELETE FROM users;"; + + private final FilmController filmController; + private final UserController userController; + private final JdbcTemplate jdbcTemplate; @BeforeEach - @DisplayName("Инициализирует контроллер") - void setUp() { - UserStorage userStorage = new InMemoryUserStorage(); - UserService userService = new UserService(userStorage); - userController = new UserController(userService); - - FilmStorage filmStorage = new InMemoryFilmStorage(); - FilmService filmService = new FilmService(filmStorage, userStorage); - filmController = new FilmController(filmService); + @DisplayName("Чистим БД") + void cleanDatabase() { + jdbcTemplate.update(DELETE_FILMS); + jdbcTemplate.update(DELETE_USERS); } @Test @DisplayName("При добавлении двух фильмов контроллер должен возвращать их") void test_getFilms_WhenAddedFilms_ShouldReturnAll() { //given - Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + NewFilmRequest film = NewFilmRequest.builder().name(FILM_NAME).description(FILM_DESCRIPTION) + .releaseDate(FILM_RELEASE_DATE) .duration(FILM_DURATION).build(); - Film film2 = Film.builder().name(FILM_NAME_2).description(FILM_DESCRIPTION_2).releaseDate(FILM_RELEASE_DATE_2) + NewFilmRequest film2 = NewFilmRequest.builder().name(FILM_NAME_2).description(FILM_DESCRIPTION_2) + .releaseDate(FILM_RELEASE_DATE_2) .duration(FILM_DURATION_2).build(); //when filmController.addFilm(film); filmController.addFilm(film2); - List filmsByController = filmController.getFilms().getBody(); + List filmsByController = filmController.getFilms().getBody(); //then assertNotNull(filmsByController, "В контроллере нет фильмов"); @@ -88,13 +98,13 @@ void test_getFilms_WhenAddedFilms_ShouldReturnAll() { @DisplayName("При добавлении фильма с корректными полями контроллер должен добавить его") void test_addFilm_WhenCorrectFields_ShouldAddToController() { //given - Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION) + NewFilmRequest film = NewFilmRequest.builder().name(FILM_NAME).description(FILM_DESCRIPTION) .releaseDate(FILM_RELEASE_DATE) .duration(FILM_DURATION).build(); //when filmController.addFilm(film); - List filmsByController = filmController.getFilms().getBody(); + List filmsByController = filmController.getFilms().getBody(); //then assertNotNull(filmsByController, "В контроллере нет фильмов"); @@ -106,7 +116,7 @@ void test_addFilm_WhenCorrectFields_ShouldAddToController() { @DisplayName("При добавлении фильма с некорректным описанием контроллер должен выбросить ошибку") void test_addFilm_WhenIncorrectDescription_ShouldThrowsError() { //given && when - Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION_INCORRECT) + NewFilmRequest film = NewFilmRequest.builder().name(FILM_NAME).description(FILM_DESCRIPTION_INCORRECT) .releaseDate(FILM_RELEASE_DATE) .duration(FILM_DURATION).build(); @@ -120,7 +130,7 @@ void test_addFilm_WhenIncorrectDescription_ShouldThrowsError() { @DisplayName("При добавлении фильма с некорректной длительностью контроллер должен выбросить ошибку") void test_addFilm_WhenIncorrectDuration_ShouldThrowsError() { //given && when - Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION) + NewFilmRequest film = NewFilmRequest.builder().name(FILM_NAME).description(FILM_DESCRIPTION) .releaseDate(FILM_RELEASE_DATE) .duration(FILM_DURATION_INCORRECT).build(); @@ -134,7 +144,7 @@ void test_addFilm_WhenIncorrectDuration_ShouldThrowsError() { @DisplayName("При добавлении фильма с некорректной датой релиза контроллер должен выбросить ошибку") void test_addFilm_WhenIncorrectReleaseDate_ShouldThrowsError() { //given && when - Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION) + NewFilmRequest film = NewFilmRequest.builder().name(FILM_NAME).description(FILM_DESCRIPTION) .releaseDate(FILM_RELEASE_DATE_INCORRECT) .duration(FILM_DURATION).build(); @@ -148,14 +158,18 @@ void test_addFilm_WhenIncorrectReleaseDate_ShouldThrowsError() { @DisplayName("При обновлении фильма с корректными данными контроллер должен обновить его") void test_updateFilm_WhenCorrectFields_ShouldUpdateInController() { //given - Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + NewFilmRequest film = NewFilmRequest.builder().name(FILM_NAME).description(FILM_DESCRIPTION) + .releaseDate(FILM_RELEASE_DATE) .duration(FILM_DURATION).build(); - filmController.addFilm(film); + int filmId = Objects.requireNonNull(filmController.addFilm(film).getBody()).getId(); //when - Film updatedFilm = film.toBuilder().name(FILM_NAME_2).build(); + UpdateFilmRequest updatedFilm = UpdateFilmRequest.builder().id(filmId).name(FILM_NAME_2) + .description(FILM_DESCRIPTION) + .releaseDate(FILM_RELEASE_DATE) + .duration(FILM_DURATION).build(); filmController.updateFilm(updatedFilm); - List filmsByController = filmController.getFilms().getBody(); + List filmsByController = filmController.getFilms().getBody(); //then assertNotNull(filmsByController, "В контроллере нет фильмов"); @@ -167,12 +181,16 @@ void test_updateFilm_WhenCorrectFields_ShouldUpdateInController() { @DisplayName("При обновлении фильма с некорректным описанием контроллер должен выбросить ошибку") void test_updateFilm_WhenIncorrectDescription_ShouldThrowsError() { //given - Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + NewFilmRequest film = NewFilmRequest.builder().name(FILM_NAME).description(FILM_DESCRIPTION) + .releaseDate(FILM_RELEASE_DATE) .duration(FILM_DURATION).build(); - filmController.addFilm(film); + int filmId = Objects.requireNonNull(filmController.addFilm(film).getBody()).getId(); //when - Film updatedFilm = film.toBuilder().description(FILM_DESCRIPTION_INCORRECT).build(); + UpdateFilmRequest updatedFilm = UpdateFilmRequest.builder().id(filmId).name(FILM_NAME_2) + .description(FILM_DESCRIPTION_INCORRECT) + .releaseDate(FILM_RELEASE_DATE) + .duration(FILM_DURATION).build(); //then assertThrows(ValidationException.class, @@ -184,13 +202,16 @@ void test_updateFilm_WhenIncorrectDescription_ShouldThrowsError() { @DisplayName("При обновлении фильма с некорректной длительностью контроллер должен выбросить ошибку") void test_updateFilm_WhenIncorrectDuration_ShouldThrowsError() { //given - Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + NewFilmRequest film = NewFilmRequest.builder().name(FILM_NAME).description(FILM_DESCRIPTION) + .releaseDate(FILM_RELEASE_DATE) .duration(FILM_DURATION).build(); - filmController.addFilm(film); + int filmId = Objects.requireNonNull(filmController.addFilm(film).getBody()).getId(); //when - Film updatedFilm = film.toBuilder().duration(FILM_DURATION_INCORRECT).build(); - + UpdateFilmRequest updatedFilm = UpdateFilmRequest.builder().id(filmId).name(FILM_NAME_2) + .description(FILM_DESCRIPTION) + .releaseDate(FILM_RELEASE_DATE) + .duration(FILM_DURATION_INCORRECT).build(); //then assertThrows(ValidationException.class, () -> filmController.updateFilm(updatedFilm), @@ -201,12 +222,16 @@ void test_updateFilm_WhenIncorrectDuration_ShouldThrowsError() { @DisplayName("При добавлении фильма с некорректной датой релиза контроллер должен выбросить ошибку") void test_updateFilm_WhenIncorrectReleaseDate_ShouldThrowsError() { //given - Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + NewFilmRequest film = NewFilmRequest.builder().name(FILM_NAME).description(FILM_DESCRIPTION) + .releaseDate(FILM_RELEASE_DATE) .duration(FILM_DURATION).build(); - filmController.addFilm(film); + int filmId = Objects.requireNonNull(filmController.addFilm(film).getBody()).getId(); //when - Film updatedFilm = film.toBuilder().releaseDate(FILM_RELEASE_DATE_INCORRECT).build(); + UpdateFilmRequest updatedFilm = UpdateFilmRequest.builder().id(filmId).name(FILM_NAME_2) + .description(FILM_DESCRIPTION) + .releaseDate(FILM_RELEASE_DATE_INCORRECT) + .duration(FILM_DURATION).build(); //then assertThrows(ValidationException.class, @@ -218,12 +243,13 @@ void test_updateFilm_WhenIncorrectReleaseDate_ShouldThrowsError() { @DisplayName("При получении существующего фильма по id контроллер должен вернуть его") void test_getFilmById_WhenFilmExists_ShouldReturnOne() { //given - Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + NewFilmRequest film = NewFilmRequest.builder().name(FILM_NAME).description(FILM_DESCRIPTION) + .releaseDate(FILM_RELEASE_DATE) .duration(FILM_DURATION).build(); - Film createdFilm = filmController.addFilm(film).getBody(); + int filmId = Objects.requireNonNull(filmController.addFilm(film).getBody()).getId(); //when - Film findFilm = filmController.getFilmById(createdFilm.getId()).getBody(); + FilmDto findFilm = filmController.getFilmById(filmId).getBody(); //then assertNotNull(findFilm, "В контроллере нет фильмов"); @@ -235,7 +261,7 @@ void test_getFilmById_WhenFilmExists_ShouldReturnOne() { void test_getFilmById_WhenFilmNotExists_ShouldThrowsError() { //given && when && then assertThrows(NotFoundException.class, - () -> filmController.getFilmById(FILM_ID).getBody(), + () -> filmController.getFilmById(FILM_ID), "Из контроллера получен несуществующий фильм"); } @@ -243,16 +269,18 @@ void test_getFilmById_WhenFilmNotExists_ShouldThrowsError() { @DisplayName("При добавлении лайка существующему фильму от существующего пользователя должен вернуться OK 200") void test_addLikeByUser_WhenFilmAndUserExist_ShouldReturn200() { //given - Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + NewFilmRequest film = NewFilmRequest.builder().name(FILM_NAME).description(FILM_DESCRIPTION) + .releaseDate(FILM_RELEASE_DATE) .duration(FILM_DURATION).build(); - Film createdFilm = filmController.addFilm(film).getBody(); + int filmId = Objects.requireNonNull(filmController.addFilm(film).getBody()).getId(); - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - User createdUser = userController.addUser(user).getBody(); + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY).build(); + int userId = Objects.requireNonNull(userController.addUser(user).getBody()).getId(); //when - ResponseEntity response = filmController.addLikeByUser(createdFilm.getId(), createdUser.getId()); + ResponseEntity response = filmController.addLikeByUser(filmId, userId); //then assertEquals(HttpStatus.OK, response.getStatusCode(), "Вернулся не успешный ответ"); @@ -262,12 +290,13 @@ void test_addLikeByUser_WhenFilmAndUserExist_ShouldReturn200() { @DisplayName("При добавлении лайка существующему фильму от несуществующего пользователя должен вернуть ошибку") void test_addLikeByUser_WhenUserNotExists_ShouldThrowsError() { //given && when - Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + NewFilmRequest film = NewFilmRequest.builder().name(FILM_NAME).description(FILM_DESCRIPTION) + .releaseDate(FILM_RELEASE_DATE) .duration(FILM_DURATION).build(); - Film createdFilm = filmController.addFilm(film).getBody(); + int filmId = Objects.requireNonNull(filmController.addFilm(film).getBody()).getId(); //then - assertThrows(NotFoundException.class, () -> filmController.addLikeByUser(createdFilm.getId(), USER_ID), + assertThrows(NotFoundException.class, () -> filmController.addLikeByUser(filmId, USER_ID), "Удалось поставить лайк от несуществующего пользователя"); } @@ -275,11 +304,12 @@ void test_addLikeByUser_WhenUserNotExists_ShouldThrowsError() { @DisplayName("При добавлении лайка несуществующему фильму от существующего пользователя должен вернуть ошибку") void test_addLikeByUser_WhenFilmNotExists_ShouldThrowsError() { //given && when - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - User createdUser = userController.addUser(user).getBody(); + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY).build(); + int userId = Objects.requireNonNull(userController.addUser(user).getBody()).getId(); //then - assertThrows(NotFoundException.class, () -> filmController.addLikeByUser(FILM_ID, createdUser.getId()), + assertThrows(NotFoundException.class, () -> filmController.addLikeByUser(FILM_ID, userId), "Удалось поставить лайк несуществующему фильму"); } @@ -287,16 +317,18 @@ void test_addLikeByUser_WhenFilmNotExists_ShouldThrowsError() { @DisplayName("При удалении лайка с существующего фильма от существующего пользователя должен вернуть OK 200") void test_removeLikeByUser_WhenFilmAndUserExist_ShouldReturn200() { //given - Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + NewFilmRequest film = NewFilmRequest.builder().name(FILM_NAME).description(FILM_DESCRIPTION) + .releaseDate(FILM_RELEASE_DATE) .duration(FILM_DURATION).build(); - Film createdFilm = filmController.addFilm(film).getBody(); + int filmId = Objects.requireNonNull(filmController.addFilm(film).getBody()).getId(); - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - User createdUser = userController.addUser(user).getBody(); + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY).build(); + int userId = Objects.requireNonNull(userController.addUser(user).getBody()).getId(); //when - filmController.addLikeByUser(createdFilm.getId(), createdUser.getId()); - ResponseEntity response = filmController.removeLikeByUser(createdFilm.getId(), createdUser.getId()); + filmController.addLikeByUser(filmId, userId); + ResponseEntity response = filmController.removeLikeByUser(filmId, userId); //then assertEquals(HttpStatus.OK, response.getStatusCode(), "Вернулся не успешный ответ"); @@ -306,12 +338,13 @@ void test_removeLikeByUser_WhenFilmAndUserExist_ShouldReturn200() { @DisplayName("При удалении лайка с существующего фильма от несуществующего пользователя должен вернуть ошибку") void test_removeLikeByUser_WhenUserNotExists_ShouldThrowsError() { //given && when - Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + NewFilmRequest film = NewFilmRequest.builder().name(FILM_NAME).description(FILM_DESCRIPTION) + .releaseDate(FILM_RELEASE_DATE) .duration(FILM_DURATION).build(); - Film createdFilm = filmController.addFilm(film).getBody(); + int filmId = Objects.requireNonNull(filmController.addFilm(film).getBody()).getId(); //then - assertThrows(NotFoundException.class, () -> filmController.removeLikeByUser(createdFilm.getId(), USER_ID), + assertThrows(NotFoundException.class, () -> filmController.removeLikeByUser(filmId, USER_ID), "Удалось убрать лайк от несуществующего пользователя"); } @@ -319,11 +352,12 @@ void test_removeLikeByUser_WhenUserNotExists_ShouldThrowsError() { @DisplayName("При удалении лайка с несуществующего фильма от существующего пользователя должен вернуть ошибку") void test_removeLikeByUser_WhenFilmNotExists_ShouldThrowsError() { //given && when - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - User createdUser = userController.addUser(user).getBody(); + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY).build(); + int userId = Objects.requireNonNull(userController.addUser(user).getBody()).getId(); //then - assertThrows(NotFoundException.class, () -> filmController.removeLikeByUser(FILM_ID, createdUser.getId()), + assertThrows(NotFoundException.class, () -> filmController.removeLikeByUser(FILM_ID, userId), "Удалось убрать лайк с несуществующего фильма"); } @@ -331,23 +365,29 @@ void test_removeLikeByUser_WhenFilmNotExists_ShouldThrowsError() { @DisplayName("При запросе одного фильма должен вернуть самый популярный") void test_getMostPopularFilms_WhenRequestOne_ShouldReturnOne() { //given - Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + NewFilmRequest film = NewFilmRequest.builder().name(FILM_NAME).description(FILM_DESCRIPTION) + .releaseDate(FILM_RELEASE_DATE) .duration(FILM_DURATION).build(); - Film film2 = Film.builder().name(FILM_NAME_2).description(FILM_DESCRIPTION_2).releaseDate(FILM_RELEASE_DATE_2) + NewFilmRequest film2 = NewFilmRequest.builder().name(FILM_NAME_2).description(FILM_DESCRIPTION_2) + .releaseDate(FILM_RELEASE_DATE_2) .duration(FILM_DURATION_2).build(); - Film createdFilm = filmController.addFilm(film).getBody(); - Film createdFilm2 = filmController.addFilm(film2).getBody(); - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - User createdUser = userController.addUser(user).getBody(); - User user2 = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - User createdUser2 = userController.addUser(user2).getBody(); + int filmId = Objects.requireNonNull(filmController.addFilm(film).getBody()).getId(); + int filmId2 = Objects.requireNonNull(filmController.addFilm(film2).getBody()).getId(); + + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY).build(); + int userId = Objects.requireNonNull(userController.addUser(user).getBody()).getId(); + + NewUserRequest user2 = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL_2).login(USER_LOGIN_2) + .birthday(USER_BIRTHDAY).build(); + int userId2 = Objects.requireNonNull(userController.addUser(user2).getBody()).getId(); //when - filmController.addLikeByUser(createdFilm.getId(), createdUser.getId()); - filmController.addLikeByUser(createdFilm2.getId(), createdUser.getId()); - filmController.addLikeByUser(createdFilm2.getId(), createdUser2.getId()); - List popularFilms = filmController.getMostPopularFilms(FILM_COUNT_1).getBody(); + filmController.addLikeByUser(filmId, userId); + filmController.addLikeByUser(filmId2, userId2); + filmController.addLikeByUser(filmId2, userId2); + List popularFilms = filmController.getMostPopularFilms(FILM_COUNT_1).getBody(); //then assertNotNull(popularFilms, "В контроллере нет популярных фильмов"); @@ -359,23 +399,29 @@ void test_getMostPopularFilms_WhenRequestOne_ShouldReturnOne() { @DisplayName("Если передано дефолтное количество фильмов, то должен вернуть все (до дефолтного включительно)") void test_getMostPopularFilms_WhenRequestNothing_ShouldReturnAll() { //given - Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + NewFilmRequest film = NewFilmRequest.builder().name(FILM_NAME).description(FILM_DESCRIPTION) + .releaseDate(FILM_RELEASE_DATE) .duration(FILM_DURATION).build(); - Film film2 = Film.builder().name(FILM_NAME_2).description(FILM_DESCRIPTION_2).releaseDate(FILM_RELEASE_DATE_2) + NewFilmRequest film2 = NewFilmRequest.builder().name(FILM_NAME_2).description(FILM_DESCRIPTION_2) + .releaseDate(FILM_RELEASE_DATE_2) .duration(FILM_DURATION_2).build(); - Film createdFilm = filmController.addFilm(film).getBody(); - Film createdFilm2 = filmController.addFilm(film2).getBody(); - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - User createdUser = userController.addUser(user).getBody(); - User user2 = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - User createdUser2 = userController.addUser(user2).getBody(); + int filmId = Objects.requireNonNull(filmController.addFilm(film).getBody()).getId(); + int filmId2 = Objects.requireNonNull(filmController.addFilm(film2).getBody()).getId(); + + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY).build(); + int userId = Objects.requireNonNull(userController.addUser(user).getBody()).getId(); + + NewUserRequest user2 = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL_2).login(USER_LOGIN_2) + .birthday(USER_BIRTHDAY).build(); + int userId2 = Objects.requireNonNull(userController.addUser(user2).getBody()).getId(); //when - filmController.addLikeByUser(createdFilm.getId(), createdUser.getId()); - filmController.addLikeByUser(createdFilm2.getId(), createdUser.getId()); - filmController.addLikeByUser(createdFilm2.getId(), createdUser2.getId()); - List popularFilms = filmController.getMostPopularFilms(FILM_DEFAULT_COUNT).getBody(); + filmController.addLikeByUser(filmId, userId); + filmController.addLikeByUser(filmId2, userId2); + filmController.addLikeByUser(filmId2, userId2); + List popularFilms = filmController.getMostPopularFilms(FILM_DEFAULT_COUNT).getBody(); //then assertNotNull(popularFilms, "В контроллере нет популярных фильмов"); 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..0dbf263 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/GenreControllerTest.java @@ -0,0 +1,48 @@ +package ru.yandex.practicum.filmorate.controller; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.DisplayName; +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.ComponentScan; +import ru.yandex.practicum.filmorate.dto.GenreDto; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@ComponentScan(basePackages = "ru.yandex.practicum.filmorate") +public class GenreControllerTest { + private static final int GENRE_ID = 1; + private static final String GENRE_NAME = "Комедия"; + private static final int GENRES_COUNT = 6; + private final GenreController genreController; + + @Test + @DisplayName("При запросе по id должен вернуть жанр") + void test_getGenreById_ShouldReturnById() { + //given && when + GenreDto genre = genreController.getGenreById(GENRE_ID).getBody(); + + //then + assertNotNull(genre, "В контроллере нет жанров"); + assertEquals(GENRE_NAME, genre.getName(), "В контроллере не верный жанр"); + } + + @Test + @DisplayName("При запросе должен вернуть все жанры") + void test_getUsers_ShouldReturnAll() { + //given && when + List genres = genreController.getGenres().getBody(); + + //then + assertNotNull(genres, "В контроллере нет жанров"); + assertEquals(GENRES_COUNT, genres.size(), "В контроллере не верное количество жанров"); + } +} \ 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..a4e0385 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/MpaControllerTest.java @@ -0,0 +1,48 @@ +package ru.yandex.practicum.filmorate.controller; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.DisplayName; +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.ComponentScan; +import ru.yandex.practicum.filmorate.dto.MpaDto; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@ComponentScan(basePackages = "ru.yandex.practicum.filmorate") +public class MpaControllerTest { + private static final int MPA_ID = 1; + private static final String MPA_NAME = "G"; + private static final int MPA_COUNT = 5; + private final MpaController mpaController; + + @Test + @DisplayName("При запросе по id должен вернуть рейтинг") + void test_getMpaById_ShouldReturnById() { + //given && when + MpaDto mpa = mpaController.getMpaById(MPA_ID).getBody(); + + //then + assertNotNull(mpa, "В контроллере нет рейтингом"); + assertEquals(MPA_NAME, mpa.getName(), "В контроллере не верный рейтинг"); + } + + @Test + @DisplayName("При запросе должен вернуть все рейтинги") + void test_getMpaList_ShouldReturnAll() { + //given && when + List mpaList = mpaController.getMpaList().getBody(); + + //then + assertNotNull(mpaList, "В контроллере нет рейтингов"); + assertEquals(MPA_COUNT, mpaList.size(), "В контроллере не верное количество рейтингов"); + } +} \ 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 3c76ba9..e68b1c5 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java @@ -1,23 +1,33 @@ package ru.yandex.practicum.filmorate.controller; +import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; 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.ComponentScan; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.jdbc.core.JdbcTemplate; +import ru.yandex.practicum.filmorate.dto.user.NewUserRequest; +import ru.yandex.practicum.filmorate.dto.user.UpdateUserRequest; +import ru.yandex.practicum.filmorate.dto.user.UserDto; 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.service.UserService; -import ru.yandex.practicum.filmorate.storage.InMemoryUserStorage; -import ru.yandex.practicum.filmorate.storage.UserStorage; import java.time.LocalDate; import java.time.Month; import java.util.List; +import java.util.Objects; import static org.junit.jupiter.api.Assertions.*; +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@ComponentScan(basePackages = "ru.yandex.practicum.filmorate") class UserControllerTest { private static final String USER_NAME = "Вася"; private static final String USER_NAME_2 = "Петя"; @@ -39,28 +49,32 @@ class UserControllerTest { private static final Integer USER_ID = 567; - UserController userController; + private static final String DELETE_USERS = "DELETE FROM users;"; + + private final UserController userController; + private final JdbcTemplate jdbcTemplate; @BeforeEach - @DisplayName("Инициализирует контроллер") - void setUp() { - UserStorage userStorage = new InMemoryUserStorage(); - UserService userService = new UserService(userStorage); - userController = new UserController(userService); + @DisplayName("Чистим БД") + void cleanDatabase() { + jdbcTemplate.update(DELETE_USERS); } @Test @DisplayName("При добавлении двух пользователей контроллер должен возвращать их") void test_getUsers_WhenAddedUsers_ShouldReturnAll() { //given - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - User user2 = User.builder().name(USER_NAME_2).email(USER_EMAIL_2).login(USER_LOGIN_2).birthday(USER_BIRTHDAY_2) - .build(); - //when + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY) + .build(); userController.addUser(user); + + NewUserRequest user2 = NewUserRequest.builder().name(USER_NAME_2).email(USER_EMAIL_2).login(USER_LOGIN_2) + .birthday(USER_BIRTHDAY_2) + .build(); userController.addUser(user2); - List usersByController = userController.getUsers().getBody(); + List usersByController = userController.getUsers().getBody(); //then assertNotNull(usersByController, "В контроллере нет пользователей"); @@ -75,11 +89,13 @@ void test_getUsers_WhenAddedUsers_ShouldReturnAll() { @DisplayName("При добавлении пользователя с корректными полями контроллер должен добавить его") void test_addUser_WhenCorrectFields_ShouldAddToController() { //given - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY) + .build(); //when userController.addUser(user); - List usersByController = userController.getUsers().getBody(); + List usersByController = userController.getUsers().getBody(); //then assertNotNull(usersByController, "В контроллере нет пользователей"); @@ -91,7 +107,7 @@ void test_addUser_WhenCorrectFields_ShouldAddToController() { @DisplayName("При добавлении пользователя с некорректным логином контроллер должен выбросить ошибку") void test_addUser_WhenIncorrectLogin_ShouldThrowsError() { //given && when - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN_INCORRECT) + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN_INCORRECT) .birthday(USER_BIRTHDAY).build(); //then @@ -104,11 +120,13 @@ void test_addUser_WhenIncorrectLogin_ShouldThrowsError() { @DisplayName("При добавлении пользователя без имени в контроллере имя будет равно логину") void test_addUser_WhenNameIsNull_ShouldReturnNameWithLoginValue() { //given && when - User user = User.builder().email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); + NewUserRequest user = NewUserRequest.builder().email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY) + .build(); //when userController.addUser(user); - List usersByController = userController.getUsers().getBody(); + List usersByController = userController.getUsers().getBody(); //then assertNotNull(usersByController, "В контроллере нет пользователей"); @@ -120,7 +138,7 @@ void test_addUser_WhenNameIsNull_ShouldReturnNameWithLoginValue() { @DisplayName("При добавлении пользователя с датой рождения в будущем контроллер должен выбросить ошибку") void test_addUser_WhenBirthdayInFuture_ShouldThrowsError() { //given && when - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) .birthday(USER_BIRTHDAY_INCORRECT).build(); //then @@ -133,13 +151,18 @@ void test_addUser_WhenBirthdayInFuture_ShouldThrowsError() { @DisplayName("При обновлении пользователя с корректными полями контроллер должен обновить его") void test_updateUser_WhenCorrectFields_ShouldUpdateInController() { //given - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - userController.addUser(user); + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY) + .build(); + int userId = Objects.requireNonNull(userController.addUser(user).getBody()).getId(); //when - User updateUser = user.toBuilder().name(USER_NAME_2).build(); + UpdateUserRequest updateUser = UpdateUserRequest.builder().id(userId).name(USER_NAME_2).email(USER_EMAIL) + .login(USER_LOGIN) + .birthday(USER_BIRTHDAY) + .build(); userController.updateUser(updateUser); - List usersByController = userController.getUsers().getBody(); + List usersByController = userController.getUsers().getBody(); //then assertNotNull(usersByController, "В контроллере нет пользователей"); @@ -151,11 +174,16 @@ void test_updateUser_WhenCorrectFields_ShouldUpdateInController() { @DisplayName("При обновлении пользователя с некорректным логином контроллер должен выбросить ошибку") void test_updateUser_WhenIncorrectLogin_ShouldThrowsError() { //given && when - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - userController.addUser(user); + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY) + .build(); + int userId = Objects.requireNonNull(userController.addUser(user).getBody()).getId(); //when - User updateUser = user.toBuilder().login(USER_LOGIN_INCORRECT).build(); + UpdateUserRequest updateUser = UpdateUserRequest.builder().id(userId).name(USER_NAME).email(USER_EMAIL) + .login(USER_LOGIN_INCORRECT) + .birthday(USER_BIRTHDAY) + .build(); //then assertThrows(ValidationException.class, @@ -163,33 +191,21 @@ void test_updateUser_WhenIncorrectLogin_ShouldThrowsError() { "Пользователь обновлён с некорректным логином"); } - @Test - @DisplayName("При обновлении пользователя без имени в контроллере имя будет равно логину") - void test_updateUser_WhenNameIsNull_ShouldReturnNameWithLoginValue() { - //given && when - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - userController.addUser(user); - - //when - User updateUser = user.toBuilder().name("").build(); - userController.updateUser(updateUser); - List usersByController = userController.getUsers().getBody(); - - //then - assertNotNull(usersByController, "В контроллере нет пользователей"); - assertEquals(1, usersByController.size(), "В контроллере не верное количество пользователей"); - assertEquals(USER_LOGIN, usersByController.get(0).getName(), "Имя пользователя не равно логину"); - } @Test @DisplayName("При обновлении пользователя с датой рождения в будущем контроллер должен выбросить ошибку") void test_updateUser_WhenBirthdayInFuture_ShouldThrowsError() { //given && when - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - userController.addUser(user); + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY) + .build(); + int userId = Objects.requireNonNull(userController.addUser(user).getBody()).getId(); //when - User updateUser = user.toBuilder().birthday(USER_BIRTHDAY_INCORRECT).build(); + UpdateUserRequest updateUser = UpdateUserRequest.builder().id(userId).name(USER_NAME).email(USER_EMAIL) + .login(USER_LOGIN) + .birthday(USER_BIRTHDAY_INCORRECT) + .build(); //then assertThrows(ValidationException.class, @@ -201,11 +217,14 @@ void test_updateUser_WhenBirthdayInFuture_ShouldThrowsError() { @DisplayName("При получении существующего пользователя по id контроллер должен вернуть его") void test_getUserById_WhenUserExists_ShouldReturnOne() { //given - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - User createdUser = userController.addUser(user).getBody(); + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY) + .build(); + int userId = Objects.requireNonNull(userController.addUser(user).getBody()).getId(); + //when - User findUser = userController.getUserById(createdUser.getId()).getBody(); + UserDto findUser = userController.getUserById(userId).getBody(); //then assertNotNull(findUser, "В контроллере нет пользователей"); @@ -225,14 +244,18 @@ void test_getUserById_WhenUserNotExists_ShouldThrowsError() { @DisplayName("При добавлении в друзья существующих пользователей ответ должен быть OK 200") void test_addFriend_WhenUsersExist_ShouldReturn200() { //given - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - User user2 = User.builder().name(USER_NAME_2).email(USER_EMAIL_2).login(USER_LOGIN_2).birthday(USER_BIRTHDAY_2) + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY) + .build(); + int userId = Objects.requireNonNull(userController.addUser(user).getBody()).getId(); + + NewUserRequest user2 = NewUserRequest.builder().name(USER_NAME_2).email(USER_EMAIL_2).login(USER_LOGIN_2) + .birthday(USER_BIRTHDAY_2) .build(); - User createdUser1 = userController.addUser(user).getBody(); - User createdUser2 = userController.addUser(user2).getBody(); + int userId2 = Objects.requireNonNull(userController.addUser(user2).getBody()).getId(); //when - ResponseEntity response = userController.addFriend(createdUser1.getId(), createdUser2.getId()); + ResponseEntity response = userController.addFriend(userId, userId2); //then assertEquals(HttpStatus.OK, response.getStatusCode(), @@ -243,12 +266,14 @@ void test_addFriend_WhenUsersExist_ShouldReturn200() { @DisplayName("При добавлении в друзья несуществующего пользователя контроллер должен выбросить ошибку") void test_addFriend_WhenOneUserNotExists_ShouldThrowsError() { //given && when - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - User createdUser1 = userController.addUser(user).getBody(); + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY) + .build(); + int userId = Objects.requireNonNull(userController.addUser(user).getBody()).getId(); //then assertThrows(NotFoundException.class, - () -> userController.addFriend(createdUser1.getId(), USER_ID), + () -> userController.addFriend(userId, USER_ID), "Удалось добавить в друзья не существующего пользователя"); } @@ -256,15 +281,19 @@ void test_addFriend_WhenOneUserNotExists_ShouldThrowsError() { @DisplayName("При удалении из друзей существующих пользователей ответ должен быть OK 200") void test_removeFriend_WhenFriendsExist_ShouldReturn200() { //given - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - User user2 = User.builder().name(USER_NAME_2).email(USER_EMAIL_2).login(USER_LOGIN_2).birthday(USER_BIRTHDAY_2) + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY) .build(); - User createdUser1 = userController.addUser(user).getBody(); - User createdUser2 = userController.addUser(user2).getBody(); - userController.addFriend(createdUser1.getId(), createdUser2.getId()); + int userId = Objects.requireNonNull(userController.addUser(user).getBody()).getId(); + + NewUserRequest user2 = NewUserRequest.builder().name(USER_NAME_2).email(USER_EMAIL_2).login(USER_LOGIN_2) + .birthday(USER_BIRTHDAY_2) + .build(); + int userId2 = Objects.requireNonNull(userController.addUser(user2).getBody()).getId(); + userController.addFriend(userId, userId2); //when - ResponseEntity response = userController.removeFriend(createdUser1.getId(), createdUser2.getId()); + ResponseEntity response = userController.removeFriend(userId, userId2); //then assertEquals(HttpStatus.OK, response.getStatusCode(), @@ -275,12 +304,14 @@ void test_removeFriend_WhenFriendsExist_ShouldReturn200() { @DisplayName("При удалении из друзей несуществующего пользователя контроллер должен выбросить ошибку") void test_removeFriend_WhenOneFriendNotExist_ShouldReturn200() { //given && when - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - User createdUser1 = userController.addUser(user).getBody(); + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY) + .build(); + int userId = Objects.requireNonNull(userController.addUser(user).getBody()).getId(); //then assertThrows(NotFoundException.class, - () -> userController.removeFriend(createdUser1.getId(), USER_ID), + () -> userController.removeFriend(userId, USER_ID), "Удалось удалить из друзей несуществующего пользователя"); } @@ -288,15 +319,19 @@ void test_removeFriend_WhenOneFriendNotExist_ShouldReturn200() { @DisplayName("При получении всех друзей пользователя должен возвращаться корректный список") void test_getAllFriends_WhenAddFriends_ShouldReturnOne() { //given - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - User user2 = User.builder().name(USER_NAME_2).email(USER_EMAIL_2).login(USER_LOGIN_2).birthday(USER_BIRTHDAY_2) + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY) + .build(); + int userId = Objects.requireNonNull(userController.addUser(user).getBody()).getId(); + + NewUserRequest user2 = NewUserRequest.builder().name(USER_NAME_2).email(USER_EMAIL_2).login(USER_LOGIN_2) + .birthday(USER_BIRTHDAY_2) .build(); - User createdUser1 = userController.addUser(user).getBody(); - User createdUser2 = userController.addUser(user2).getBody(); + int userId2 = Objects.requireNonNull(userController.addUser(user2).getBody()).getId(); //when - userController.addFriend(createdUser1.getId(), createdUser2.getId()); - List userFriends = userController.getAllFriends(createdUser1.getId()).getBody(); + userController.addFriend(userId, userId2); + List userFriends = userController.getAllFriends(userId).getBody(); //then assertNotNull(userFriends, "У пользователя нет друзей"); @@ -307,11 +342,13 @@ void test_getAllFriends_WhenAddFriends_ShouldReturnOne() { @DisplayName("При отсутствии друзей у пользователя должен возвращаться пустой список") void test_getAllFriends_WhenNoFriends_ShouldReturnEmpty() { //given - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - User createdUser1 = userController.addUser(user).getBody(); + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY) + .build(); + int userId = Objects.requireNonNull(userController.addUser(user).getBody()).getId(); //when - List userFriends = userController.getAllFriends(createdUser1.getId()).getBody(); + List userFriends = userController.getAllFriends(userId).getBody(); //then assertNotNull(userFriends, "Список друзей не проинициализировался"); @@ -322,19 +359,25 @@ void test_getAllFriends_WhenNoFriends_ShouldReturnEmpty() { @DisplayName("При получении общих друзей пользователей должен возвращаться корректный список") void test_getCommonFriends_WhenAddSameFriend_ShouldReturnOne() { //given - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - User user2 = User.builder().name(USER_NAME_2).email(USER_EMAIL_2).login(USER_LOGIN_2).birthday(USER_BIRTHDAY_2) + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY) .build(); - User user3 = User.builder().name(USER_NAME_3).email(USER_EMAIL_3).login(USER_LOGIN_3).birthday(USER_BIRTHDAY_3) + int userId = Objects.requireNonNull(userController.addUser(user).getBody()).getId(); + + NewUserRequest user2 = NewUserRequest.builder().name(USER_NAME_2).email(USER_EMAIL_2).login(USER_LOGIN_2) + .birthday(USER_BIRTHDAY_2) .build(); - User createdUser1 = userController.addUser(user).getBody(); - User createdUser2 = userController.addUser(user2).getBody(); - User createdUser3 = userController.addUser(user3).getBody(); + int userId2 = Objects.requireNonNull(userController.addUser(user2).getBody()).getId(); + + NewUserRequest user3 = NewUserRequest.builder().name(USER_NAME_3).email(USER_EMAIL_3).login(USER_LOGIN_3) + .birthday(USER_BIRTHDAY_3) + .build(); + int userId3 = Objects.requireNonNull(userController.addUser(user3).getBody()).getId(); //when - userController.addFriend(createdUser1.getId(), createdUser3.getId()); - userController.addFriend(createdUser2.getId(), createdUser3.getId()); - List commonFriends = userController.getCommonFriends(createdUser1.getId(), createdUser2.getId()).getBody(); + userController.addFriend(userId, userId3); + userController.addFriend(userId2, userId3); + List commonFriends = userController.getCommonFriends(userId, userId2).getBody(); //then assertNotNull(commonFriends, "У пользователя нет общих друзей"); @@ -345,19 +388,25 @@ void test_getCommonFriends_WhenAddSameFriend_ShouldReturnOne() { @DisplayName("При отсутствии общих друзей у пользователей должен возвращаться пустой список") void test_getCommonFriends_WhenAddNotSameFriend_ShouldReturnEmpty() { //given - User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); - User user2 = User.builder().name(USER_NAME_2).email(USER_EMAIL_2).login(USER_LOGIN_2).birthday(USER_BIRTHDAY_2) + NewUserRequest user = NewUserRequest.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN) + .birthday(USER_BIRTHDAY) .build(); - User user3 = User.builder().name(USER_NAME_3).email(USER_EMAIL_3).login(USER_LOGIN_3).birthday(USER_BIRTHDAY_3) + int userId = Objects.requireNonNull(userController.addUser(user).getBody()).getId(); + + NewUserRequest user2 = NewUserRequest.builder().name(USER_NAME_2).email(USER_EMAIL_2).login(USER_LOGIN_2) + .birthday(USER_BIRTHDAY_2) + .build(); + int userId2 = Objects.requireNonNull(userController.addUser(user2).getBody()).getId(); + + NewUserRequest user3 = NewUserRequest.builder().name(USER_NAME_3).email(USER_EMAIL_3).login(USER_LOGIN_3) + .birthday(USER_BIRTHDAY_3) .build(); - User createdUser1 = userController.addUser(user).getBody(); - User createdUser2 = userController.addUser(user2).getBody(); - User createdUser3 = userController.addUser(user3).getBody(); + int userId3 = Objects.requireNonNull(userController.addUser(user3).getBody()).getId(); //when - userController.addFriend(createdUser1.getId(), createdUser1.getId()); - userController.addFriend(createdUser2.getId(), createdUser3.getId()); - List commonFriends = userController.getCommonFriends(createdUser1.getId(), createdUser2.getId()).getBody(); + userController.addFriend(userId, userId2); + userController.addFriend(userId2, userId3); + List commonFriends = userController.getCommonFriends(userId, userId2).getBody(); //then assertNotNull(commonFriends, "Список общих друзей не проинициализировался"); diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/dao/FilmDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/dao/FilmDbStorageTest.java new file mode 100644 index 0000000..18c2e09 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/dao/FilmDbStorageTest.java @@ -0,0 +1,346 @@ +package ru.yandex.practicum.filmorate.storage.dao; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +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.ComponentScan; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.jdbc.core.JdbcTemplate; +import ru.yandex.practicum.filmorate.exception.InternalServerException; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.FilmStorage; +import ru.yandex.practicum.filmorate.storage.UserStorage; + +import java.time.LocalDate; +import java.time.Month; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@ComponentScan(basePackages = "ru.yandex.practicum.filmorate") +class FilmDbStorageTest { + private static final String FILM_NAME = "Фильм"; + private static final String FILM_NAME_2 = "Второй фильм"; + + private static final String FILM_DESCRIPTION = "Описание фильма"; + private static final String FILM_DESCRIPTION_2 = "Описание второго фильма"; + private static final String FILM_DESCRIPTION_INCORRECT = "Очень " + "очень".repeat(199) + "длинное описание"; + + private static final LocalDate FILM_RELEASE_DATE = LocalDate.of(1992, Month.DECEMBER, 12); + private static final LocalDate FILM_RELEASE_DATE_2 = LocalDate.of(2005, Month.APRIL, 28); + private static final LocalDate FILM_RELEASE_DATE_INCORRECT = LocalDate.of(1600, Month.DECEMBER, 5); + + private static final int FILM_DURATION = 100; + private static final int FILM_DURATION_2 = 200; + private static final int FILM_DURATION_INCORRECT = -100; + + private static final int FILM_ID = 100; + private static final int FILM_COUNT_1 = 1; + + private static final String USER_NAME = "Вася"; + private static final String USER_EMAIL = "mail@mail.ru"; + private static final String USER_EMAIL_2 = "newMail@mail.ru"; + private static final String USER_LOGIN = "user"; + private static final String USER_LOGIN_2 = "newUser"; + private static final LocalDate USER_BIRTHDAY = LocalDate.of(1992, Month.DECEMBER, 12); + private static final Integer USER_ID = 567; + + private static final String DELETE_FILMS = "DELETE FROM films;"; + private static final String DELETE_USERS = "DELETE FROM users;"; + + private final FilmStorage filmStorage; + private final UserStorage userStorage; + private final JdbcTemplate jdbcTemplate; + + @BeforeEach + @DisplayName("Чистим БД") + void cleanDatabase() { + jdbcTemplate.update(DELETE_FILMS); + jdbcTemplate.update(DELETE_USERS); + } + + @Test + @DisplayName("При получении существующего фильма по id хранилище должно вернуть его") + void test_getFilmById_WhenFilmExists_ShouldReturnOne() { + //given + Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + .duration(FILM_DURATION).build(); + Film createdFilm = filmStorage.addFilm(film); + + //when + Film findFilm = filmStorage.getFilmById(createdFilm.getId()); + + //then + assertNotNull(findFilm, "В БД нет фильмов"); + assertEquals(FILM_NAME, findFilm.getName(), "В БД некорректный фильм"); + } + + @Test + @DisplayName("При получении несуществующего фильма по id хранилище должно вернуть ошибку") + void test_getFilmById_WhenFilmNotExists_ShouldThrowsError() { + //given && when && then + assertThrows(NotFoundException.class, + () -> filmStorage.getFilmById(FILM_ID), + "Из БД получен несуществующий фильм"); + } + + @Test + @DisplayName("При добавлении двух фильмов хранилище должно возвращать их") + void test_getFilms_WhenAddedFilms_ShouldReturnAll() { + //given + Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + .duration(FILM_DURATION).build(); + Film film2 = Film.builder().name(FILM_NAME_2).description(FILM_DESCRIPTION_2).releaseDate(FILM_RELEASE_DATE_2) + .duration(FILM_DURATION_2).build(); + + //when + filmStorage.addFilm(film); + filmStorage.addFilm(film2); + List filmsByController = filmStorage.getFilms(); + + //then + assertNotNull(filmsByController, "В БД нет фильмов"); + assertEquals(2, filmsByController.size(), "В БД не верное количество фильмов"); + assertEquals(FILM_NAME, filmsByController.get(0).getName(), "В БД не корректный 1-ый фильм"); + assertEquals(FILM_NAME_2, filmsByController.get(1).getName(), "В БД не корректный 2-ой фильм"); + } + + @Test + @DisplayName("При добавлении фильма с корректными полями хранилище должно добавить его") + void test_addFilm_WhenCorrectFields_ShouldAddToController() { + //given + Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION) + .releaseDate(FILM_RELEASE_DATE) + .duration(FILM_DURATION).build(); + + //when + filmStorage.addFilm(film); + List filmsByStorage = filmStorage.getFilms(); + + //then + assertNotNull(filmsByStorage, "В БД нет фильмов"); + assertEquals(1, filmsByStorage.size(), "В БД не верное количество фильмов"); + assertEquals(FILM_NAME, filmsByStorage.get(0).getName(), "В БД некорректный фильм"); + } + + @Test + @DisplayName("При добавлении фильма с некорректным описанием хранилище должно выбросить ошибку") + void test_addFilm_WhenIncorrectDescription_ShouldThrowsError() { + //given && when + Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION_INCORRECT) + .releaseDate(FILM_RELEASE_DATE) + .duration(FILM_DURATION).build(); + + //then + assertThrows(DataIntegrityViolationException.class, + () -> filmStorage.addFilm(film), + "Фильм может быть добавлен с некорректным описанием"); + } + + @Test + @DisplayName("При добавлении фильма с некорректной длительностью хранилище должно выбросить ошибку") + void test_addFilm_WhenIncorrectDuration_ShouldThrowsError() { + //given && when + Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION) + .releaseDate(FILM_RELEASE_DATE) + .duration(FILM_DURATION_INCORRECT).build(); + + //then + assertThrows(DataIntegrityViolationException.class, + () -> filmStorage.addFilm(film), + "Фильм может быть добавлен с некорректной длительностью"); + } + + @Test + @DisplayName("При добавлении фильма с некорректной датой релиза хранилище должно выбросить ошибку") + void test_addFilm_WhenIncorrectReleaseDate_ShouldThrowsError() { + //given && when + Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION) + .releaseDate(FILM_RELEASE_DATE_INCORRECT) + .duration(FILM_DURATION).build(); + + //then + assertThrows(DataIntegrityViolationException.class, + () -> filmStorage.addFilm(film), + "Фильм может быть добавлен с некорректной датой релиза"); + } + + @Test + @DisplayName("При обновлении фильма с корректными данными хранилище должно обновить его") + void test_updateFilm_WhenCorrectFields_ShouldUpdateInController() { + //given + Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + .duration(FILM_DURATION).build(); + filmStorage.addFilm(film); + + //when + Film updatedFilm = film.toBuilder().name(FILM_NAME_2).build(); + filmStorage.updateFilm(updatedFilm); + List filmsByController = filmStorage.getFilms(); + + //then + assertNotNull(filmsByController, "В БД нет фильмов"); + assertEquals(1, filmsByController.size(), "В БД не верное количество фильмов"); + assertEquals(FILM_NAME_2, filmsByController.get(0).getName(), "В БД некорректный фильм"); + } + + @Test + @DisplayName("При обновлении фильма с некорректным описанием хранилище должно выбросить ошибку") + void test_updateFilm_WhenIncorrectDescription_ShouldThrowsError() { + //given + Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + .duration(FILM_DURATION).build(); + filmStorage.addFilm(film); + + //when + Film updatedFilm = film.toBuilder().description(FILM_DESCRIPTION_INCORRECT).build(); + + //then + assertThrows(DataIntegrityViolationException.class, + () -> filmStorage.updateFilm(updatedFilm), + "Фильм может быть обновлён с некорректным описанием"); + } + + @Test + @DisplayName("При обновлении фильма с некорректной длительностью хранилище должно выбросить ошибку") + void test_updateFilm_WhenIncorrectDuration_ShouldThrowsError() { + //given + Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + .duration(FILM_DURATION).build(); + filmStorage.addFilm(film); + + //when + Film updatedFilm = film.toBuilder().duration(FILM_DURATION_INCORRECT).build(); + + //then + assertThrows(DataIntegrityViolationException.class, + () -> filmStorage.updateFilm(updatedFilm), + "Фильм может быть обновлён с некорректной длительностью"); + } + + @Test + @DisplayName("При добавлении фильма с некорректной датой релиза хранилище должно выбросить ошибку") + void test_updateFilm_WhenIncorrectReleaseDate_ShouldThrowsError() { + //given + Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + .duration(FILM_DURATION).build(); + filmStorage.addFilm(film); + + //when + Film updatedFilm = film.toBuilder().releaseDate(FILM_RELEASE_DATE_INCORRECT).build(); + + //then + assertThrows(DataIntegrityViolationException.class, + () -> filmStorage.updateFilm(updatedFilm), + "Фильм может быть обновлён с некорректной датой релиза"); + } + + @Test + @DisplayName("При добавлении лайка существующему фильму от существующего пользователя должен вернуться OK") + void test_addLikeByUser_WhenFilmAndUserExist_ShouldReturn200() { + //given && when + Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + .duration(FILM_DURATION).build(); + Film createdFilm = filmStorage.addFilm(film); + + User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); + User createdUser = userStorage.addUser(user); + + //then + assertDoesNotThrow(() -> filmStorage.addLike(createdFilm.getId(), createdUser.getId()), + "Не удалось поставить лайк существующему фильму"); + } + + @Test + @DisplayName("При добавлении лайка несуществующему фильму от существующего пользователя должен вернуть ошибку") + void test_addLikeByUser_WhenFilmNotExists_ShouldThrowsError() { + //given && when + User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); + User createdUser = userStorage.addUser(user); + + //then + assertThrows(DataIntegrityViolationException.class, () -> filmStorage.addLike(FILM_ID, createdUser.getId()), + "Удалось поставить лайк несуществующему фильму"); + } + + @Test + @DisplayName("При удалении лайка с существующего фильма от существующего пользователя должен вернуть OK") + void test_removeLikeByUser_WhenFilmAndUserExist_ShouldReturnOk() { + //given + Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + .duration(FILM_DURATION).build(); + Film createdFilm = filmStorage.addFilm(film); + + User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); + User createdUser = userStorage.addUser(user); + + //when + filmStorage.addLike(createdFilm.getId(), createdUser.getId()); + + //then + assertDoesNotThrow(() -> filmStorage.removeLike(createdFilm.getId(), createdUser.getId()), + "Не удалось поставить лайк существующему фильму"); + } + + @Test + @DisplayName("При удалении лайка с существующего фильма от несуществующего пользователя должен вернуть ошибку") + void test_removeLikeByUser_WhenUserNotExists_ShouldThrowsError() { + //given && when + Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + .duration(FILM_DURATION).build(); + Film createdFilm = filmStorage.addFilm(film); + + //then + assertThrows(InternalServerException.class, () -> filmStorage.removeLike(createdFilm.getId(), USER_ID), + "Удалось убрать лайк от несуществующего пользователя"); + } + + @Test + @DisplayName("При удалении лайка с несуществующего фильма от существующего пользователя должен вернуть ошибку") + void test_removeLikeByUser_WhenFilmNotExists_ShouldThrowsError() { + //given && when + User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); + User createdUser = userStorage.addUser(user); + + //then + assertThrows(InternalServerException.class, () -> filmStorage.removeLike(FILM_ID, createdUser.getId()), + "Удалось убрать лайк с несуществующего фильма"); + } + + @Test + @DisplayName("При запросе одного фильма должен вернуть самый популярный") + void test_getMostPopularFilms_WhenRequestOne_ShouldReturnOne() { + //given + Film film = Film.builder().name(FILM_NAME).description(FILM_DESCRIPTION).releaseDate(FILM_RELEASE_DATE) + .duration(FILM_DURATION).build(); + Film film2 = Film.builder().name(FILM_NAME_2).description(FILM_DESCRIPTION_2).releaseDate(FILM_RELEASE_DATE_2) + .duration(FILM_DURATION_2).build(); + Film createdFilm = filmStorage.addFilm(film); + Film createdFilm2 = filmStorage.addFilm(film2); + + User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); + User createdUser = userStorage.addUser(user); + User user2 = User.builder().name(USER_NAME).email(USER_EMAIL_2).login(USER_LOGIN_2).birthday(USER_BIRTHDAY).build(); + User createdUser2 = userStorage.addUser(user2); + + //when + filmStorage.addLike(createdFilm.getId(), createdUser.getId()); + filmStorage.addLike(createdFilm2.getId(), createdUser.getId()); + filmStorage.addLike(createdFilm2.getId(), createdUser2.getId()); + List popularFilms = filmStorage.getMostPopularFilms(FILM_COUNT_1); + + //then + assertNotNull(popularFilms, "В БД нет популярных фильмов"); + assertEquals(1, popularFilms.size(), "В БД не верное количество фильмов"); + assertEquals(FILM_NAME_2, popularFilms.get(0).getName(), "В БД не корректный фильм"); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/dao/GenreDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/dao/GenreDbStorageTest.java new file mode 100644 index 0000000..012ed08 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/dao/GenreDbStorageTest.java @@ -0,0 +1,48 @@ +package ru.yandex.practicum.filmorate.storage.dao; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.DisplayName; +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.ComponentScan; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.storage.GenreStorage; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@ComponentScan(basePackages = "ru.yandex.practicum.filmorate") +class GenreDbStorageTest { + private static final int GENRE_ID = 1; + private static final String GENRE_NAME = "Комедия"; + private static final int GENRES_COUNT = 6; + private final GenreStorage genreStorage; + + @Test + @DisplayName("При запросе по id должен вернуть жанр") + void test_getGenreById_ShouldReturnById() { + //given && when + Genre genre = genreStorage.getGenreById(GENRE_ID); + + //then + assertNotNull(genre, "В контроллере нет жанров"); + assertEquals(GENRE_NAME, genre.getName(), "В контроллере не верный жанр"); + } + + @Test + @DisplayName("При запросе должен вернуть все жанры") + void test_getUsers_ShouldReturnAll() { + //given && when + List genres = genreStorage.getGenres(); + + //then + assertNotNull(genres, "В контроллере нет жанров"); + assertEquals(GENRES_COUNT, genres.size(), "В контроллере не верное количество жанров"); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/dao/MpaDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/dao/MpaDbStorageTest.java new file mode 100644 index 0000000..d255b4c --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/dao/MpaDbStorageTest.java @@ -0,0 +1,48 @@ +package ru.yandex.practicum.filmorate.storage.dao; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.DisplayName; +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.ComponentScan; +import ru.yandex.practicum.filmorate.model.Mpa; +import ru.yandex.practicum.filmorate.storage.MpaStorage; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@ComponentScan(basePackages = "ru.yandex.practicum.filmorate") +class MpaDbStorageTest { + private static final int MPA_ID = 1; + private static final String MPA_NAME = "G"; + private static final int MPA_COUNT = 5; + private final MpaStorage mpaStorage; + + @Test + @DisplayName("При запросе по id должен вернуть жанр") + void test_getMpaById_ShouldReturnById() { + //given && when + Mpa mpa = mpaStorage.getMpaById(MPA_ID); + + //then + assertNotNull(mpa, "В контроллере нет рейтингом"); + assertEquals(MPA_NAME, mpa.getName(), "В контроллере не верный рейтинг"); + } + + @Test + @DisplayName("При запросе должен вернуть все жанры") + void test_getMpaList_ShouldReturnAll() { + //given && when + List mpaList = mpaStorage.getMpaList(); + + //then + assertNotNull(mpaList, "В контроллере нет рейтингов"); + assertEquals(MPA_COUNT, mpaList.size(), "В контроллере не верное количество рейтингов"); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/dao/UserDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/dao/UserDbStorageTest.java new file mode 100644 index 0000000..98d5fbe --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/dao/UserDbStorageTest.java @@ -0,0 +1,186 @@ +package ru.yandex.practicum.filmorate.storage.dao; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +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.ComponentScan; +import org.springframework.jdbc.core.JdbcTemplate; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.UserStorage; + +import java.time.LocalDate; +import java.time.Month; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@ComponentScan(basePackages = "ru.yandex.practicum.filmorate") +class UserDbStorageTest { + private static final String USER_NAME = "Вася"; + private static final String USER_NAME_2 = "Петя"; + + private static final String USER_EMAIL = "mail@mail.ru"; + private static final String USER_EMAIL_2 = "newMail@mail.ru"; + + private static final String USER_LOGIN = "user"; + private static final String USER_LOGIN_2 = "newUser"; + + private static final LocalDate USER_BIRTHDAY = LocalDate.of(1992, Month.DECEMBER, 12); + private static final LocalDate USER_BIRTHDAY_2 = LocalDate.of(1982, Month.OCTOBER, 22); + + private static final Integer USER_ID = 567; + private static final String DELETE_USERS = "DELETE FROM users;"; + + private final UserStorage userStorage; + private final JdbcTemplate jdbcTemplate; + + @BeforeEach + @DisplayName("Чистим БД") + void cleanDatabase() { + jdbcTemplate.update(DELETE_USERS); + } + + @Test + @DisplayName("При получении существующего пользователя по id хранилище должно вернуть его") + void test_getUserById_WhenUserExists_ShouldReturnOne() { + //given + User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); + User createdUser = userStorage.addUser(user); + + //when + User findUser = userStorage.getUserById(createdUser.getId()); + + //then + assertNotNull(findUser, "В БД нет пользователей"); + assertEquals(USER_NAME, findUser.getName(), "В БД некорректный пользователь"); + } + + @Test + @DisplayName("При получении несуществующего пользователя по id хранилище должно выбросить ошибку") + void test_getUserById_WhenUserNotExists_ShouldThrowsError() { + //given && when && then + assertThrows(NotFoundException.class, + () -> userStorage.getUserById(USER_ID), + "Из контроллера получен несуществующий пользователь"); + } + + @Test + @DisplayName("При добавлении двух пользователей хранилище должно возвращать их") + void test_getUsers_WhenAddedUsers_ShouldReturnAll() { + //given + User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); + User user2 = User.builder().name(USER_NAME_2).email(USER_EMAIL_2).login(USER_LOGIN_2).birthday(USER_BIRTHDAY_2) + .build(); + + //when + userStorage.addUser(user); + userStorage.addUser(user2); + List usersByController = userStorage.getUsers(); + + //then + assertNotNull(usersByController, "В БД нет пользователей"); + assertEquals(2, usersByController.size(), "В БД не верное количество пользователей"); + assertEquals(USER_NAME, usersByController.get(0).getName(), + "В БД не корректный 1-ый пользователь"); + assertEquals(USER_NAME_2, usersByController.get(1).getName(), + "В БД не корректный 2-ой пользователь"); + } + + @Test + @DisplayName("При добавлении пользователя с корректными полями хранилище должно добавить его") + void test_addUser_WhenCorrectFields_ShouldAddToStorage() { + //given + User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); + + //when + userStorage.addUser(user); + List usersByController = userStorage.getUsers(); + + //then + assertNotNull(usersByController, "В БД нет пользователей"); + assertEquals(1, usersByController.size(), "В БД не верное количество пользователей"); + assertEquals(USER_NAME, usersByController.get(0).getName(), "В БД некорректный пользователь"); + } + + @Test + @DisplayName("При добавлении в друзья существующих пользователей список друзей должен обновиться") + void test_addFriends_WhenUsersExist_ShouldReturnOne() { + //given + User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); + User user2 = User.builder().name(USER_NAME_2).email(USER_EMAIL_2).login(USER_LOGIN_2).birthday(USER_BIRTHDAY_2) + .build(); + User createdUser1 = userStorage.addUser(user); + User createdUser2 = userStorage.addUser(user2); + + //when + userStorage.addFriends(createdUser1.getId(), createdUser2.getId()); + List friendsId = new ArrayList<>(userStorage.getUserFriends(createdUser1.getId())); + + //then + assertEquals(1, friendsId.size(), + "При добавлении в друзья список друзей остался пуст"); + } + + @Test + @DisplayName("При удалении из друзей пользователя список друзей должен обновиться") + void test_removeFriends_WhenFriendsExist_ShouldReturnZero() { + //given + User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); + User user2 = User.builder().name(USER_NAME_2).email(USER_EMAIL_2).login(USER_LOGIN_2).birthday(USER_BIRTHDAY_2) + .build(); + User createdUser1 = userStorage.addUser(user); + User createdUser2 = userStorage.addUser(user2); + userStorage.addFriends(createdUser1.getId(), createdUser2.getId()); + + //when + userStorage.removeFriends(createdUser1.getId(), createdUser2.getId()); + List friendsId = new ArrayList<>(userStorage.getUserFriends(createdUser1.getId())); + + //then + assertEquals(0, friendsId.size(), + "При удалении из друзей пользователь остался в друзьях"); + } + + @Test + @DisplayName("При получении всех друзей пользователя должен возвращаться корректный список") + void test_getUserFriends_WhenAddFriends_ShouldReturnOne() { + //given + User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); + User user2 = User.builder().name(USER_NAME_2).email(USER_EMAIL_2).login(USER_LOGIN_2).birthday(USER_BIRTHDAY_2) + .build(); + User createdUser1 = userStorage.addUser(user); + User createdUser2 = userStorage.addUser(user2); + + //when + userStorage.addFriends(createdUser1.getId(), createdUser2.getId()); + List friendsId = new ArrayList<>(userStorage.getUserFriends(createdUser1.getId())); + + //then + assertEquals(1, friendsId.size(), + "При добавлении в друзья список друзей остался пуст"); + } + + @Test + @DisplayName("При отсутствии друзей у пользователя должен возвращаться пустой список") + void test_getUserFriends_WhenNoFriends_ShouldReturnEmpty() { + //given + User user = User.builder().name(USER_NAME).email(USER_EMAIL).login(USER_LOGIN).birthday(USER_BIRTHDAY).build(); + User createdUser1 = userStorage.addUser(user); + + //when + List userFriends = new ArrayList<>(userStorage.getUserFriends(createdUser1.getId())); + + //then + assertNotNull(userFriends, "Список друзей не проинициализировался"); + assertEquals(0, userFriends.size(), "Список друзей не пустой"); + } +} \ No newline at end of file