diff --git a/filmorate_database.png b/filmorate_database.png new file mode 100644 index 0000000..364b4cf Binary files /dev/null and b/filmorate_database.png differ diff --git a/pom.xml b/pom.xml index 0cad031..c21376e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,14 +5,12 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 - + 3.2.5 + ru.yandex.practicum filmorate 0.0.1-SNAPSHOT - filmorate - filmorate 21 @@ -21,19 +19,30 @@ org.springframework.boot spring-boot-starter-web - + + org.springframework.boot + spring-boot-starter-validation + org.projectlombok lombok - provided + true org.springframework.boot spring-boot-starter-test test + + com.h2database + h2 + runtime + + + org.springframework.boot + spring-boot-starter-jdbc + - @@ -42,5 +51,4 @@ - - + \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java b/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java index dca451b..8f860a2 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java +++ b/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java @@ -8,5 +8,4 @@ public class FilmorateApplication { public static void main(String[] args) { SpringApplication.run(FilmorateApplication.class, args); } - -} +} \ No newline at end of file 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 08cf0a1..aefc6f3 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -1,7 +1,49 @@ package ru.yandex.practicum.filmorate.controller; - -import org.springframework.web.bind.annotation.RestController; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.service.FilmService; +import java.util.List; @RestController +@RequestMapping("/films") +@RequiredArgsConstructor public class FilmController { -} + private final FilmService filmService; + + @PostMapping + public Film create(@Valid @RequestBody Film film) { + return filmService.create(film); + } + + @PutMapping + public Film update(@Valid @RequestBody Film film) { + return filmService.update(film); + } + + @GetMapping + public List getAll() { + return filmService.getAll(); + } + + @GetMapping("/{id}") + public Film getById(@PathVariable int id) { + return filmService.getById(id); + } + + @PutMapping("/{id}/like/{userId}") + public void addLike(@PathVariable int id, @PathVariable int userId) { + filmService.addLike(id, userId); + } + + @DeleteMapping("/{id}/like/{userId}") + public void removeLike(@PathVariable int id, @PathVariable int userId) { + filmService.removeLike(id, userId); + } + + @GetMapping("/popular") + public List getPopularFilms(@RequestParam(defaultValue = "10", required = false) int count) { + return filmService.getPopularFilms(count); + } +} \ No newline at end of file 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..5a75f88 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java @@ -0,0 +1,29 @@ +package ru.yandex.practicum.filmorate.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.service.GenreService; + +import java.util.List; + +@RestController +@RequestMapping("/genres") +@RequiredArgsConstructor +public class GenreController { + + private final GenreService genreService; + + @GetMapping + public List getAll() { + return genreService.getAll(); + } + + @GetMapping("/{id}") + public Genre getById(@PathVariable int id) { + return genreService.getById(id); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java new file mode 100644 index 0000000..0824690 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java @@ -0,0 +1,29 @@ +package ru.yandex.practicum.filmorate.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import ru.yandex.practicum.filmorate.model.MpaRating; +import ru.yandex.practicum.filmorate.service.MpaService; + +import java.util.List; + +@RestController +@RequestMapping("/mpa") +@RequiredArgsConstructor +public class MpaController { + + private final MpaService mpaService; + + @GetMapping + public List getAll() { + return mpaService.getAll(); + } + + @GetMapping("/{id}") + public MpaRating getById(@PathVariable int id) { + return mpaService.getById(id); + } +} \ 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 new file mode 100644 index 0000000..0e226d0 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -0,0 +1,58 @@ +package ru.yandex.practicum.filmorate.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.service.UserService; + +import java.util.List; + +@RestController +@RequestMapping("/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + private static final String FRIEND_PATH = "/{id}/friends/{friendId}"; + + @PostMapping + public User create(@Valid @RequestBody User user) { + return userService.create(user); + } + + @PutMapping + public User update(@Valid @RequestBody User user) { + return userService.update(user); + } + + @GetMapping + public List getAll() { + return userService.getAll(); + } + + @GetMapping("/{id}") + public User getById(@PathVariable int id) { + return userService.getById(id); + } + + @PutMapping(FRIEND_PATH) + public void addFriend(@PathVariable int id, @PathVariable int friendId) { + userService.addFriend(id, friendId); + } + + @DeleteMapping(FRIEND_PATH) + public void removeFriend(@PathVariable int id, @PathVariable int friendId) { + userService.removeFriend(id, friendId); + } + + @GetMapping("/{id}/friends") + public List getFriends(@PathVariable int id) { + return userService.getFriends(id); + } + + @GetMapping("/{id}/friends/common/{otherId}") + public List getCommonFriends(@PathVariable int id, @PathVariable int otherId) { + return userService.getCommonFriends(id, otherId); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/error/ErrorHandler.java b/src/main/java/ru/yandex/practicum/filmorate/error/ErrorHandler.java new file mode 100644 index 0000000..dcf6964 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/error/ErrorHandler.java @@ -0,0 +1,45 @@ +package ru.yandex.practicum.filmorate.error; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ValidationException; + +@RestControllerAdvice +@Slf4j +public class ErrorHandler { + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleValidationException(final MethodArgumentNotValidException e) { + log.error("Ошибка валидации: {}", e.getMessage()); + return new ErrorResponse( + e.getBindingResult().getFieldError().getDefaultMessage() + ); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleCustomValidationException(final ValidationException e) { + log.error("Ошибка валидации: {}", e.getMessage()); + return new ErrorResponse(e.getMessage()); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorResponse handleNotFoundException(final NotFoundException e) { + log.error("Объект не найден: {}", e.getMessage()); + return new ErrorResponse(e.getMessage()); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorResponse handleThrowable(final Throwable e) { + log.error("Внутренняя ошибка сервера: ", e); + return new ErrorResponse("Произошла непредвиденная ошибка."); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/error/ErrorResponse.java b/src/main/java/ru/yandex/practicum/filmorate/error/ErrorResponse.java new file mode 100644 index 0000000..9aa31ba --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/error/ErrorResponse.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.error; + +public class ErrorResponse { + private final String error; + + public ErrorResponse(String error) { + this.error = error; + } + + public String getError() { + return error; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/NotFoundException.java b/src/main/java/ru/yandex/practicum/filmorate/exception/NotFoundException.java new file mode 100644 index 0000000..82e3f72 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/ValidationException.java b/src/main/java/ru/yandex/practicum/filmorate/exception/ValidationException.java new file mode 100644 index 0000000..532f9bd --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ValidationException.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.exception; + +public class ValidationException extends RuntimeException { + public ValidationException(String message) { + super(message); + } +} \ 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 3614a44..dae0ab0 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -1,12 +1,32 @@ package ru.yandex.practicum.filmorate.model; -import lombok.Getter; -import lombok.Setter; - -/** - * Film. - */ -@Getter -@Setter +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDate; +import java.util.LinkedHashSet; +import java.util.Set; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) public class Film { -} + int id; + + @NotBlank(message = "Название не может быть пустым.") + String name; + + @Size(max = 200, message = "Описание не должно превышать 200 символов.") + String description; + + LocalDate releaseDate; + + @Positive(message = "Продолжительность фильма должна быть положительной.") + int duration; + + MpaRating mpa; + Set genres = new LinkedHashSet<>(); // Используем LinkedHashSet для сохранения порядка +} \ 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..0f6a5f1 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java @@ -0,0 +1,16 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class Genre { + int id; + String name; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/MpaRating.java b/src/main/java/ru/yandex/practicum/filmorate/model/MpaRating.java new file mode 100644 index 0000000..6b60b91 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/MpaRating.java @@ -0,0 +1,16 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class MpaRating { + int id; + String name; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/User.java b/src/main/java/ru/yandex/practicum/filmorate/model/User.java new file mode 100644 index 0000000..df67aae --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java @@ -0,0 +1,30 @@ +package ru.yandex.practicum.filmorate.model; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.PastOrPresent; +import jakarta.validation.constraints.Pattern; +import lombok.AccessLevel; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDate; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +public class User { + int id; + + @NotBlank(message = "Email не может быть пустым.") + @Email(message = "Email должен быть в правильном формате.") + String email; + + @NotBlank(message = "Логин не может быть пустым.") + @Pattern(regexp = "\\S*", message = "Логин не может содержать пробелы.") + String login; + + String name; + + @PastOrPresent(message = "Дата рождения не может быть в будущем.") + LocalDate birthday; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java new file mode 100644 index 0000000..136ee49 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -0,0 +1,80 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ValidationException; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Genre; +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.UserStorage; + +import java.time.LocalDate; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class FilmService { + + private static final LocalDate EARLIEST_RELEASE_DATE = LocalDate.of(1895, 12, 28); + private final FilmStorage filmStorage; + private final UserStorage userStorage; + private final GenreStorage genreStorage; + private final MpaStorage mpaStorage; + + public Film create(Film film) { + validateFilm(film); + return filmStorage.create(film); + } + + public Film update(Film film) { + getById(film.getId()); + validateFilm(film); + return filmStorage.update(film); + } + + public Film getById(int id) { + return filmStorage.getById(id) + .orElseThrow(() -> new NotFoundException(String.format("Фильм с id=%d не найден.", id))); + } + + public List getAll() { + return filmStorage.getAll(); + } + + public void addLike(int filmId, int userId) { + getById(filmId); + userStorage.getById(userId) + .orElseThrow(() -> new NotFoundException(String.format("Пользователь с id=%d не найден.", userId))); + filmStorage.addLike(filmId, userId); + } + + public void removeLike(int filmId, int userId) { + getById(filmId); + userStorage.getById(userId) + .orElseThrow(() -> new NotFoundException(String.format("Пользователь с id=%d не найден.", userId))); + filmStorage.removeLike(filmId, userId); + } + + public List getPopularFilms(int count) { + return filmStorage.getPopularFilms(count); + } + + private void validateFilm(Film film) { + if (film.getReleaseDate() != null && film.getReleaseDate().isBefore(EARLIEST_RELEASE_DATE)) { + throw new ValidationException("Дата релиза не может быть раньше 28 декабря 1895 года."); + } + if (film.getMpa() != null) { + mpaStorage.getById(film.getMpa().getId()) + .orElseThrow(() -> new NotFoundException(String.format("Рейтинг MPA с id=%d не найден.", film.getMpa().getId()))); + } + if (film.getGenres() != null) { + for (Genre genre : film.getGenres()) { + genreStorage.getById(genre.getId()) + .orElseThrow(() -> new NotFoundException(String.format("Жанр с id=%d не найден.", genre.getId()))); + } + } + } +} \ No newline at end of file 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..7b145e7 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java @@ -0,0 +1,25 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.storage.GenreStorage; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class GenreService { + + private final GenreStorage genreStorage; + + public List getAll() { + return genreStorage.getAll(); + } + + public Genre getById(int id) { + return genreStorage.getById(id) + .orElseThrow(() -> new NotFoundException("Жанр с id=" + id + " не найден.")); + } +} \ 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..0f72001 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java @@ -0,0 +1,25 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.MpaRating; +import ru.yandex.practicum.filmorate.storage.MpaStorage; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class MpaService { + + private final MpaStorage mpaStorage; + + public List getAll() { + return mpaStorage.getAll(); + } + + public MpaRating getById(int id) { + return mpaStorage.getById(id) + .orElseThrow(() -> new NotFoundException("Рейтинг MPA с id=" + id + " не найден.")); + } +} \ 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 new file mode 100644 index 0000000..c2f3832 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -0,0 +1,62 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.UserStorage; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserStorage userStorage; + + public User create(User user) { + if (user.getName() == null || user.getName().isBlank()) { + user.setName(user.getLogin()); + } + return userStorage.create(user); + } + + public User update(User user) { + getById(user.getId()); + if (user.getName() == null || user.getName().isBlank()) { + user.setName(user.getLogin()); + } + return userStorage.update(user); + } + + public User getById(int id) { + return userStorage.getById(id) + .orElseThrow(() -> new NotFoundException("Пользователь с id=" + id + " не найден.")); + } + + public List getAll() { + return userStorage.getAll(); + } + + public void addFriend(int userId, int friendId) { + getById(userId); + getById(friendId); + userStorage.addFriend(userId, friendId); + } + + public void removeFriend(int userId, int friendId) { + // Добавляем проверку, чтобы вернуть 404, если пользователь не найден + getById(userId); + getById(friendId); + userStorage.removeFriend(userId, friendId); + } + + public List getFriends(int userId) { + getById(userId); + return userStorage.getFriends(userId); + } + + public List getCommonFriends(int userId, int otherId) { + return userStorage.getCommonFriends(userId, otherId); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java new file mode 100644 index 0000000..8830cbf --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java @@ -0,0 +1,24 @@ +package ru.yandex.practicum.filmorate.storage; + +import ru.yandex.practicum.filmorate.model.Film; +import java.util.List; +import java.util.Optional; + +public interface FilmStorage { + Film create(Film film); + + Film update(Film film); + + Optional getById(int id); + + List getAll(); + + void delete(int id); + + // НОВЫЕ МЕТОДЫ ДЛЯ ЛАЙКОВ + void addLike(int filmId, int userId); + + void removeLike(int filmId, int userId); + + List getPopularFilms(int count); +} \ No newline at end of file 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..b36227e --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/GenreStorage.java @@ -0,0 +1,11 @@ +package ru.yandex.practicum.filmorate.storage; + +import ru.yandex.practicum.filmorate.model.Genre; +import java.util.List; +import java.util.Optional; + +public interface GenreStorage { + List getAll(); + + Optional getById(int id); +} \ 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..bdee512 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/MpaStorage.java @@ -0,0 +1,11 @@ +package ru.yandex.practicum.filmorate.storage; + +import ru.yandex.practicum.filmorate.model.MpaRating; +import java.util.List; +import java.util.Optional; + +public interface MpaStorage { + List getAll(); + + Optional getById(int id); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java new file mode 100644 index 0000000..5db9853 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java @@ -0,0 +1,26 @@ +package ru.yandex.practicum.filmorate.storage; + +import ru.yandex.practicum.filmorate.model.User; +import java.util.List; +import java.util.Optional; + +public interface UserStorage { + User create(User user); + + User update(User user); + + Optional getById(int id); + + List getAll(); + + void delete(int id); + + + void addFriend(int userId, int friendId); + + void removeFriend(int userId, int friendId); + + List getFriends(int userId); + + List getCommonFriends(int userId, int otherId); +} \ 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..26e5f50 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/dao/FilmDbStorage.java @@ -0,0 +1,147 @@ +package ru.yandex.practicum.filmorate.storage.dao; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.MpaRating; +import ru.yandex.practicum.filmorate.storage.FilmStorage; + +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.stream.Collectors; + +@Repository +@Primary +@RequiredArgsConstructor +public class FilmDbStorage implements FilmStorage { + + private final JdbcTemplate jdbcTemplate; + + @Override + public Film create(Film film) { + String sql = "INSERT INTO films (name, description, release_date, duration, mpa_rating_id) VALUES (?, ?, ?, ?, ?)"; + KeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"}); + ps.setString(1, film.getName()); + ps.setString(2, film.getDescription()); + ps.setDate(3, Date.valueOf(film.getReleaseDate())); + ps.setInt(4, film.getDuration()); + ps.setInt(5, film.getMpa().getId()); + return ps; + }, keyHolder); + + film.setId(Objects.requireNonNull(keyHolder.getKey()).intValue()); + updateFilmGenres(film); + return getById(film.getId()).orElse(null); + } + + @Override + public Film update(Film film) { + String sql = "UPDATE films SET name = ?, description = ?, release_date = ?, duration = ?, mpa_rating_id = ? WHERE id = ?"; + jdbcTemplate.update(sql, + film.getName(), + film.getDescription(), + film.getReleaseDate(), + film.getDuration(), + film.getMpa().getId(), + film.getId()); + updateFilmGenres(film); + return getById(film.getId()).orElse(null); + } + + @Override + public void delete(int id) { + String sql = "DELETE FROM films WHERE id = ?"; + jdbcTemplate.update(sql, id); + } + + @Override + public Optional getById(int id) { + String sql = "SELECT f.*, m.name AS mpa_name " + + "FROM films AS f " + + "JOIN mpa_ratings AS m ON f.mpa_rating_id = m.id " + + "WHERE f.id = ?"; + List films = jdbcTemplate.query(sql, this::mapRowToFilm, id); + return films.stream().findFirst(); + } + + @Override + public List getAll() { + String sql = "SELECT f.*, m.name AS mpa_name " + + "FROM films AS f " + + "JOIN mpa_ratings AS m ON f.mpa_rating_id = m.id " + + "ORDER BY f.id"; + return jdbcTemplate.query(sql, this::mapRowToFilm); + } + + @Override + public void addLike(int filmId, int userId) { + String sql = "INSERT INTO film_likes (film_id, user_id) VALUES (?, ?)"; + jdbcTemplate.update(sql, filmId, userId); + } + + @Override + public void removeLike(int filmId, int userId) { + String sql = "DELETE FROM film_likes WHERE film_id = ? AND user_id = ?"; + jdbcTemplate.update(sql, filmId, userId); + } + + @Override + public List getPopularFilms(int count) { + String sql = "SELECT f.id FROM films AS f " + + "LEFT JOIN film_likes AS l ON f.id = l.film_id " + + "GROUP BY f.id " + + "ORDER BY COUNT(l.user_id) DESC " + + "LIMIT ?"; + List filmIds = jdbcTemplate.queryForList(sql, Integer.class, count); + return filmIds.stream() + .map(this::getById) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + } + + private void updateFilmGenres(Film film) { + jdbcTemplate.update("DELETE FROM film_genres WHERE film_id = ?", film.getId()); + + if (film.getGenres() == null || film.getGenres().isEmpty()) { + return; + } + Set uniqueGenres = new HashSet<>(film.getGenres()); + + StringBuilder sqlBuilder = new StringBuilder("INSERT INTO film_genres (film_id, genre_id) VALUES "); + List values = uniqueGenres.stream() + .map(genre -> String.format("(%d, %d)", film.getId(), genre.getId())) + .collect(Collectors.toList()); + sqlBuilder.append(String.join(", ", values)); + + jdbcTemplate.update(sqlBuilder.toString()); + } + + private Film mapRowToFilm(ResultSet resultSet, int rowNum) throws SQLException { + Film film = new Film(); + film.setId(resultSet.getInt("id")); + film.setName(resultSet.getString("name")); + film.setDescription(resultSet.getString("description")); + film.setReleaseDate(resultSet.getDate("release_date").toLocalDate()); + film.setDuration(resultSet.getInt("duration")); + film.setMpa(new MpaRating(resultSet.getInt("mpa_rating_id"), resultSet.getString("mpa_name"))); + + String genresSql = "SELECT g.id, g.name FROM genres AS g JOIN film_genres AS fg ON g.id = fg.genre_id WHERE fg.film_id = ?"; + List genres = jdbcTemplate.query(genresSql, (g_rs, g_rowNum) -> + new Genre(g_rs.getInt("id"), g_rs.getString("name")), film.getId()); + film.getGenres().addAll(genres); + + return film; + } +} \ 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..352f616 --- /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 lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.storage.GenreStorage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Optional; + +@Repository +@Primary +@RequiredArgsConstructor +public class GenreDbStorage implements GenreStorage { + + private final JdbcTemplate jdbcTemplate; + + @Override + public List getAll() { + String sql = "SELECT * FROM genres ORDER BY id"; + return jdbcTemplate.query(sql, this::mapRowToGenre); + } + + @Override + public Optional getById(int id) { + String sql = "SELECT * FROM genres WHERE id = ?"; + List genres = jdbcTemplate.query(sql, this::mapRowToGenre, id); + return genres.stream().findFirst(); + } + + private Genre mapRowToGenre(ResultSet resultSet, int rowNum) throws SQLException { + return new Genre( + resultSet.getInt("id"), + resultSet.getString("name") + ); + } +} \ No newline at end of file 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..4c3b296 --- /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 lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.MpaRating; +import ru.yandex.practicum.filmorate.storage.MpaStorage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Optional; + +@Repository +@Primary +@RequiredArgsConstructor +public class MpaDbStorage implements MpaStorage { + + private final JdbcTemplate jdbcTemplate; + + @Override + public List getAll() { + String sql = "SELECT * FROM mpa_ratings ORDER BY id"; + return jdbcTemplate.query(sql, this::mapRowToMpaRating); + } + + @Override + public Optional getById(int id) { + String sql = "SELECT * FROM mpa_ratings WHERE id = ?"; + List ratings = jdbcTemplate.query(sql, this::mapRowToMpaRating, id); + return ratings.stream().findFirst(); + } + + private MpaRating mapRowToMpaRating(ResultSet resultSet, int rowNum) throws SQLException { + return new MpaRating( + resultSet.getInt("id"), + resultSet.getString("name") + ); + } +} \ No newline at end of file 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..a61e902 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/dao/UserDbStorage.java @@ -0,0 +1,113 @@ +package ru.yandex.practicum.filmorate.storage.dao; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.UserStorage; + +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +@Repository +@Primary +@RequiredArgsConstructor +public class UserDbStorage implements UserStorage { + + private final JdbcTemplate jdbcTemplate; + + @Override + public User create(User user) { + String sql = "INSERT INTO users (email, login, name, birthday) VALUES (?, ?, ?, ?)"; + KeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"}); + ps.setString(1, user.getEmail()); + ps.setString(2, user.getLogin()); + ps.setString(3, user.getName()); + ps.setDate(4, Date.valueOf(user.getBirthday())); + return ps; + }, keyHolder); + + user.setId(Objects.requireNonNull(keyHolder.getKey()).intValue()); + return user; + } + + @Override + public User update(User user) { + String sql = "UPDATE users SET email = ?, login = ?, name = ?, birthday = ? WHERE id = ?"; + jdbcTemplate.update(sql, + user.getEmail(), + user.getLogin(), + user.getName(), + user.getBirthday(), + user.getId()); + return getById(user.getId()).orElse(null); + } + + @Override + public void delete(int id) { + String sql = "DELETE FROM users WHERE id = ?"; + jdbcTemplate.update(sql, id); + } + + @Override + public Optional getById(int id) { + String sql = "SELECT * FROM users WHERE id = ?"; + List users = jdbcTemplate.query(sql, this::mapRowToUser, id); + return users.stream().findFirst(); + } + + @Override + public List getAll() { + String sql = "SELECT * FROM users ORDER BY id"; + return jdbcTemplate.query(sql, this::mapRowToUser); + } + + @Override + public void addFriend(int userId, int friendId) { + String sql = "INSERT INTO friendships (user_id, friend_id, status) VALUES (?, ?, ?)"; + jdbcTemplate.update(sql, userId, friendId, true); + } + + @Override + public void removeFriend(int userId, int friendId) { + String sql = "DELETE FROM friendships WHERE user_id = ? AND friend_id = ?"; + jdbcTemplate.update(sql, userId, friendId); + } + + @Override + public List getFriends(int userId) { + String sql = "SELECT u.* FROM users u JOIN friendships f ON u.id = f.friend_id WHERE f.user_id = ?"; + return jdbcTemplate.query(sql, this::mapRowToUser, userId); + } + + @Override + public List getCommonFriends(int userId, int otherId) { + String sql = "SELECT u.* " + + "FROM friendships f1 " + + "JOIN friendships f2 ON f1.friend_id = f2.friend_id " + + "JOIN users u ON f1.friend_id = u.id " + + "WHERE f1.user_id = ? AND f2.user_id = ?"; + return jdbcTemplate.query(sql, this::mapRowToUser, userId, otherId); + } + + private User mapRowToUser(ResultSet resultSet, int rowNum) throws SQLException { + User user = new User(); + user.setId(resultSet.getInt("id")); + user.setEmail(resultSet.getString("email")); + user.setLogin(resultSet.getString("login")); + user.setName(resultSet.getString("name")); + user.setBirthday(resultSet.getDate("birthday").toLocalDate()); + return user; + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b13789..3922898 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,13 @@ +# Logbook configuration +logging.level.org.zalando.logbook=TRACE +# ???????? ????????????? ?? ??? ?????? ?????????? +spring.sql.init.mode=always +# ????????? ??? ??????????? ? H2 ? ???????? ?????? +spring.datasource.url=jdbc:h2:file:./db/filmorate +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password + +# ???????? ???-??????? H2 ??? ??????? (?????????????, ?? ????? ??????) +spring.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..a5fd197 --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,14 @@ +-- Заполняем справочник MPA-рейтингов. Используем MERGE для идемпотентности. +MERGE INTO mpa_ratings (id, name) VALUES (1, 'G'); +MERGE INTO mpa_ratings (id, name) VALUES (2, 'PG'); +MERGE INTO mpa_ratings (id, name) VALUES (3, 'PG-13'); +MERGE INTO mpa_ratings (id, name) VALUES (4, 'R'); +MERGE INTO mpa_ratings (id, name) VALUES (5, 'NC-17'); + +-- Заполняем справочник жанров. +MERGE INTO genres (id, name) VALUES (1, 'Комедия'); +MERGE INTO genres (id, name) VALUES (2, 'Драма'); +MERGE INTO genres (id, name) VALUES (3, 'Мультфильм'); +MERGE INTO genres (id, name) VALUES (4, 'Триллер'); +MERGE INTO genres (id, name) VALUES (5, 'Документальный'); +MERGE INTO genres (id, name) VALUES (6, 'Боевик'); \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..0c0d075 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,51 @@ +-- Удаляем таблицы в правильном порядке, если они существуют +DROP TABLE IF EXISTS film_likes, film_genres, friendships, films, users, mpa_ratings, genres CASCADE; + +-- Справочники +CREATE TABLE IF NOT EXISTS mpa_ratings ( + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(10) NOT NULL UNIQUE +); + +CREATE TABLE IF NOT EXISTS genres ( + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(50) NOT NULL UNIQUE +); + +-- Основные сущности +CREATE TABLE IF NOT EXISTS films ( + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description VARCHAR(200), + release_date DATE, + duration INT, + mpa_rating_id INT REFERENCES mpa_ratings(id) ON DELETE RESTRICT +); + +CREATE TABLE IF NOT EXISTS users ( + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, + login VARCHAR(255) NOT NULL UNIQUE, + name VARCHAR(255), + birthday DATE +); + +-- Таблицы-связки +CREATE TABLE IF NOT EXISTS film_genres ( + film_id INT REFERENCES films(id) ON DELETE CASCADE, + genre_id INT REFERENCES genres(id) ON DELETE RESTRICT, + PRIMARY KEY (film_id, genre_id) +); + +CREATE TABLE IF NOT EXISTS film_likes ( + film_id INT REFERENCES films(id) ON DELETE CASCADE, + user_id INT REFERENCES users(id) ON DELETE CASCADE, + PRIMARY KEY (film_id, user_id) +); + +CREATE TABLE IF NOT EXISTS friendships ( + user_id INT REFERENCES users(id) ON DELETE CASCADE, + friend_id INT REFERENCES users(id) ON DELETE CASCADE, + status BOOLEAN NOT NULL, + PRIMARY KEY (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 deleted file mode 100644 index 660412e..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package ru.yandex.practicum.filmorate; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class FilmorateApplicationTests { - - @Test - void contextLoads() { - } - -} 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..c52907f --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/dao/FilmDbStorageTest.java @@ -0,0 +1,136 @@ +package ru.yandex.practicum.filmorate.storage.dao; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.MpaRating; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.UserStorage; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class FilmDbStorageTest { + + private final FilmDbStorage filmStorage; + private final UserStorage userStorage; + private final JdbcTemplate jdbcTemplate; + + @BeforeEach + void clearDb() { + // Очищаем ВСЕ таблицы перед каждым тестом + jdbcTemplate.update("DELETE FROM film_likes"); + jdbcTemplate.update("DELETE FROM film_genres"); + jdbcTemplate.update("DELETE FROM friendships"); + jdbcTemplate.update("DELETE FROM films"); + jdbcTemplate.update("DELETE FROM users"); + jdbcTemplate.execute("ALTER TABLE films ALTER COLUMN id RESTART WITH 1"); + jdbcTemplate.execute("ALTER TABLE users ALTER COLUMN id RESTART WITH 1"); + } + + @Test + public void testCreateAndGetFilmById() { + Film film = createFilm(1); + Film createdFilm = filmStorage.create(film); + Optional filmOptional = filmStorage.getById(createdFilm.getId()); + + assertThat(filmOptional) + .isPresent() + .hasValueSatisfying(f -> { + assertThat(f).hasFieldOrPropertyWithValue("name", "Film 1"); + assertThat(f.getMpa()).hasFieldOrPropertyWithValue("id", 1); + }); + } + + @Test + public void testGetAllFilms() { + filmStorage.create(createFilm(1)); + filmStorage.create(createFilm(2)); + List films = filmStorage.getAll(); + assertThat(films).hasSize(2); + } + + @Test + public void testUpdateFilm() { + Film film = filmStorage.create(createFilm(1)); + film.setName("Updated Film Name"); + filmStorage.update(film); + Optional updatedFilmOpt = filmStorage.getById(film.getId()); + + assertThat(updatedFilmOpt) + .isPresent() + .hasValueSatisfying(f -> assertThat(f).hasFieldOrPropertyWithValue("name", "Updated Film Name")); + } + + @Test + public void testDeleteFilm() { + Film film = filmStorage.create(createFilm(1)); + filmStorage.delete(film.getId()); + Optional deletedFilmOpt = filmStorage.getById(film.getId()); + assertThat(deletedFilmOpt).isEmpty(); + } + + @Test + public void testAddAndRemoveLike() { + Film film = filmStorage.create(createFilm(1)); + User user = userStorage.create(createUser(1)); + + filmStorage.addLike(film.getId(), user.getId()); + + List popular = filmStorage.getPopularFilms(1); + assertThat(popular).hasSize(1); + assertThat(popular.get(0).getId()).isEqualTo(film.getId()); + + filmStorage.removeLike(film.getId(), user.getId()); + } + + @Test + public void testGetPopularFilms() { + Film film1 = filmStorage.create(createFilm(1)); + Film film2 = filmStorage.create(createFilm(2)); + Film film3 = filmStorage.create(createFilm(3)); + User user1 = userStorage.create(createUser(1)); + User user2 = userStorage.create(createUser(2)); + + filmStorage.addLike(film2.getId(), user1.getId()); + filmStorage.addLike(film3.getId(), user1.getId()); + filmStorage.addLike(film3.getId(), user2.getId()); + + List popularFilms = filmStorage.getPopularFilms(3); + + assertThat(popularFilms).hasSize(3); + assertThat(popularFilms.get(0).getId()).isEqualTo(film3.getId()); + assertThat(popularFilms.get(1).getId()).isEqualTo(film2.getId()); + assertThat(popularFilms.get(2).getId()).isEqualTo(film1.getId()); + } + + private Film createFilm(int index) { + Film film = new Film(); + film.setName("Film " + index); + film.setDescription("Description " + index); + film.setReleaseDate(LocalDate.of(2000 + index, 1, 1)); + film.setDuration(100 + index); + film.setMpa(new MpaRating(1, "G")); + return film; + } + + private User createUser(int index) { + User user = new User(); + user.setEmail("user" + index + "@mail.com"); + user.setLogin("login" + index); + user.setName("Name " + index); + user.setBirthday(LocalDate.of(1990 + index, 1, 1)); + return user; + } +} \ 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..75767ec --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/dao/GenreDbStorageTest.java @@ -0,0 +1,40 @@ +package ru.yandex.practicum.filmorate.storage.dao; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import ru.yandex.practicum.filmorate.model.Genre; + +import java.util.List; +import java.util.Optional; +import static org.assertj.core.api.Assertions.assertThat; +@SpringBootTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class GenreDbStorageTest { + private final GenreDbStorage genreStorage; + + @Test + void testGetAllGenres() { + List genres = genreStorage.getAll(); + assertThat(genres).hasSize(6); // По data.sql у нас 6 жанров + } + + @Test + void testGetGenreById() { + Optional genreOptional = genreStorage.getById(1); + assertThat(genreOptional) + .isPresent() + .hasValueSatisfying(genre -> { + assertThat(genre).hasFieldOrPropertyWithValue("id", 1); + assertThat(genre).hasFieldOrPropertyWithValue("name", "Комедия"); + }); + } + + @Test + void testGetGenreByNonExistentId() { + Optional genreOptional = genreStorage.getById(999); + assertThat(genreOptional).isEmpty(); + } +} \ 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..6e71a13 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/dao/MpaDbStorageTest.java @@ -0,0 +1,44 @@ +package ru.yandex.practicum.filmorate.storage.dao; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import ru.yandex.practicum.filmorate.model.MpaRating; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class MpaDbStorageTest { + + private final MpaDbStorage mpaStorage; + + @Test + void testGetAllMpaRatings() { + List ratings = mpaStorage.getAll(); + assertThat(ratings).hasSize(5); // По data.sql у нас 5 рейтингов + } + + @Test + void testGetMpaRatingById() { + Optional ratingOptional = mpaStorage.getById(1); + assertThat(ratingOptional) + .isPresent() + .hasValueSatisfying(rating -> { + assertThat(rating).hasFieldOrPropertyWithValue("id", 1); + assertThat(rating).hasFieldOrPropertyWithValue("name", "G"); + }); + } + + @Test + void testGetMpaRatingByNonExistentId() { + Optional ratingOptional = mpaStorage.getById(999); + assertThat(ratingOptional).isEmpty(); + } +} \ 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..813b890 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/dao/UserDbStorageTest.java @@ -0,0 +1,121 @@ +package ru.yandex.practicum.filmorate.storage.dao; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import ru.yandex.practicum.filmorate.model.User; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class UserDbStorageTest { + + private final UserDbStorage userStorage; + private final JdbcTemplate jdbcTemplate; + + // Этот метод будет очищать таблицы перед каждым тестом + @BeforeEach + void clearDb() { + jdbcTemplate.update("DELETE FROM friendships"); + jdbcTemplate.update("DELETE FROM users"); + jdbcTemplate.execute("ALTER TABLE users ALTER COLUMN id RESTART WITH 1"); + } + + @Test + public void testCreateAndGetUserById() { + User user = createUser(1); + User createdUser = userStorage.create(user); + + Optional userOptional = userStorage.getById(createdUser.getId()); + + assertThat(userOptional) + .isPresent() + .hasValueSatisfying(u -> { + assertThat(u).hasFieldOrPropertyWithValue("id", createdUser.getId()); + assertThat(u).hasFieldOrPropertyWithValue("email", "user1@mail.com"); + }); + } + + @Test + public void testGetAllUsers() { + userStorage.create(createUser(1)); + userStorage.create(createUser(2)); + List users = userStorage.getAll(); + assertThat(users).hasSize(2); + } + + @Test + public void testUpdateUser() { + User user = userStorage.create(createUser(1)); + user.setName("Updated Name"); + userStorage.update(user); + Optional updatedUserOpt = userStorage.getById(user.getId()); + + assertThat(updatedUserOpt) + .isPresent() + .hasValueSatisfying(u -> assertThat(u).hasFieldOrPropertyWithValue("name", "Updated Name")); + } + + @Test + public void testDeleteUser() { + User user = userStorage.create(createUser(1)); + userStorage.delete(user.getId()); + Optional deletedUserOpt = userStorage.getById(user.getId()); + assertThat(deletedUserOpt).isEmpty(); + } + + @Test + public void testAddAndGetFriends() { + User user1 = userStorage.create(createUser(1)); + User user2 = userStorage.create(createUser(2)); + userStorage.addFriend(user1.getId(), user2.getId()); + + List friends = userStorage.getFriends(user1.getId()); + assertThat(friends).hasSize(1); + assertThat(friends.get(0).getId()).isEqualTo(user2.getId()); + } + + @Test + public void testRemoveFriend() { + User user1 = userStorage.create(createUser(1)); + User user2 = userStorage.create(createUser(2)); + userStorage.addFriend(user1.getId(), user2.getId()); + userStorage.removeFriend(user1.getId(), user2.getId()); + + List friends = userStorage.getFriends(user1.getId()); + assertThat(friends).isEmpty(); + } + + @Test + public void testGetCommonFriends() { + User user1 = userStorage.create(createUser(1)); + User user2 = userStorage.create(createUser(2)); + User commonFriend = userStorage.create(createUser(3)); + userStorage.addFriend(user1.getId(), commonFriend.getId()); + userStorage.addFriend(user2.getId(), commonFriend.getId()); + + List commonFriends = userStorage.getCommonFriends(user1.getId(), user2.getId()); + assertThat(commonFriends).hasSize(1); + assertThat(commonFriends.get(0).getId()).isEqualTo(commonFriend.getId()); + } + + + private User createUser(int index) { + User user = new User(); + user.setEmail("user" + index + "@mail.com"); + user.setLogin("login" + index); + user.setName("Name " + index); + user.setBirthday(LocalDate.of(1990 + index, 1, 1)); + return user; + } +} \ No newline at end of file