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