diff --git a/README.md b/README.md
index 8b13789..bd3c7cc 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,73 @@
+
+## Схема БД (ER-диаграмма)
+
+
+
+## Описание таблиц
+
+Основу данных составляют таблицы `user` и `film`, которые хранят информацию о пользователях и фильмах соответственно.
+
+Для хранения стандартизированных значений, таких как жанры и возрастные рейтинги MPA, были созданы отдельные таблицы-справочники: `genre` и `mpa_rating`. Такой подход позволяет избежать дублирования данных и упрощает их обновление.
+
+Связи "многие-ко-многим" реализуются через промежуточные таблицы. Таблица `film_genre` связывает фильмы с их жанрами. `film_like` отслеживает, какие пользователи поставили лайки фильмам, что необходимо для расчета популярности. Таблица `friendship` реализует механику дружбы между пользователями, включая статус подтверждения (`is_confirmed`).
+
+## Примеры SQL-запросов
+
+### Получение всех фильмов с их жанрами и рейтингом MPA
+
+```sql
+SELECT
+ f.id,
+ f.name,
+ f.description,
+ f.release_date,
+ f.duration,
+ mr.name AS mpa_rating,
+ GROUP_CONCAT(g.name SEPARATOR ', ') AS genres
+FROM
+ film AS f
+JOIN
+ mpa_rating AS mr ON f.mpa_rating_id = mr.id
+LEFT JOIN
+ film_genre AS fg ON f.id = fg.film_id
+LEFT JOIN
+ genre AS g ON fg.genre_id = g.id
+GROUP BY
+ f.id;
+```
+
+### Запрос на получение 10 самых популярных фильмов
+
+```sql
+SELECT
+ f.id,
+ f.name,
+ COUNT(fl.user_id) AS likes_count
+FROM
+ film AS f
+LEFT JOIN
+ film_like AS fl ON f.id = fl.film_id
+GROUP BY
+ f.id
+ORDER BY
+ likes_count DESC
+LIMIT 10;
+```
+
+### Поиск общих друзей для двух пользователей (id 1 и 2)
+
+```sql
+SELECT
+ u.*
+FROM
+ friendship AS f1
+JOIN
+ friendship AS f2 ON f1.friend_id = f2.friend_id
+JOIN
+ user AS u ON f1.friend_id = u.id
+WHERE
+ f1.user_id = 1 AND f2.user_id = 2
+ AND f1.is_confirmed = true AND f2.is_confirmed = true;
+```
+
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..cca16ab 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,32 +1,53 @@
-
+
4.0.0
+
org.springframework.boot
spring-boot-starter-parent
- 3.2.4
-
+ 3.2.0
+
+
ru.yandex.practicum
filmorate
- 0.0.1-SNAPSHOT
- filmorate
- filmorate
+ 1.0.0
+
- 21
+ 17
+ 3.9.0
+
+
org.springframework.boot
spring-boot-starter-web
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+
+ org.zalando
+ logbook-spring-boot-starter
+ ${logbook.version}
+
+
+
org.projectlombok
lombok
provided
+
+
org.springframework.boot
spring-boot-starter-test
@@ -40,7 +61,26 @@
org.springframework.boot
spring-boot-maven-plugin
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+
+
-
+
+
+ central
+ https://repo.maven.apache.org/maven2
+
+
+
\ 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..e138bc5 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,57 @@
package ru.yandex.practicum.filmorate.controller;
-import org.springframework.web.bind.annotation.RestController;
+import jakarta.validation.Valid;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+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")
+@Slf4j
public class FilmController {
-}
+ private final FilmService filmService;
+
+ @Autowired
+ public FilmController(FilmService filmService) {
+ this.filmService = filmService;
+ }
+
+ @GetMapping
+ public List getAllFilms() {
+ return filmService.getAll();
+ }
+
+ @GetMapping("/{id}")
+ public Film getFilmById(@PathVariable int id) {
+ return filmService.getById(id);
+ }
+
+ @PostMapping
+ public Film createFilm(@Valid @RequestBody Film film) {
+ return filmService.create(film);
+ }
+
+ @PutMapping
+ public Film updateFilm(@Valid @RequestBody Film film) {
+ return filmService.update(film);
+ }
+
+ @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") int count) {
+ return filmService.getPopularFilms(count);
+ }
+}
\ 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..961e226
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java
@@ -0,0 +1,62 @@
+package ru.yandex.practicum.filmorate.controller;
+
+import jakarta.validation.Valid;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+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")
+@Slf4j
+public class UserController {
+ private final UserService userService;
+
+ @Autowired
+ public UserController(UserService userService) {
+ this.userService = userService;
+ }
+
+ @GetMapping
+ public List getAllUsers() {
+ return userService.getAll();
+ }
+
+ @GetMapping("/{id}")
+ public User getUserById(@PathVariable int id) {
+ return userService.getById(id);
+ }
+
+ @PostMapping
+ public User createUser(@Valid @RequestBody User user) {
+ return userService.create(user);
+ }
+
+ @PutMapping
+ public User updateUser(@Valid @RequestBody User user) {
+ return userService.update(user);
+ }
+
+ @PutMapping("/{id}/friends/{friendId}")
+ public void addFriend(@PathVariable int id, @PathVariable int friendId) {
+ userService.addFriend(id, friendId);
+ }
+
+ @DeleteMapping("/{id}/friends/{friendId}")
+ 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..ecb293e 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,33 @@
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 com.fasterxml.jackson.annotation.JsonIgnore;
+
+import java.time.LocalDate;
+import java.util.HashSet;
+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;
+
+ @JsonIgnore
+ Set likes = new HashSet<>();
+}
\ 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..f360e27
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java
@@ -0,0 +1,36 @@
+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 com.fasterxml.jackson.annotation.JsonIgnore;
+
+import java.time.LocalDate;
+import java.util.HashSet;
+import java.util.Set;
+
+@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;
+
+ @JsonIgnore
+ Set friends = new HashSet<>();
+}
\ 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..c10bd71
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java
@@ -0,0 +1,93 @@
+package ru.yandex.practicum.filmorate.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import ru.yandex.practicum.filmorate.exception.NotFoundException;
+import ru.yandex.practicum.filmorate.exception.ValidationException;
+import ru.yandex.practicum.filmorate.model.Film;
+import ru.yandex.practicum.filmorate.storage.FilmStorage;
+import ru.yandex.practicum.filmorate.storage.UserStorage;
+
+import java.time.LocalDate;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+@Slf4j
+public class FilmService {
+ private static final LocalDate EARLIEST_RELEASE_DATE = LocalDate.of(1895, 12, 28);
+
+ private final FilmStorage filmStorage;
+ private final UserStorage userStorage;
+
+ @Autowired
+ public FilmService(FilmStorage filmStorage, UserStorage userStorage) {
+ this.filmStorage = filmStorage;
+ this.userStorage = userStorage;
+ }
+
+ public Film create(Film film) {
+ validateFilm(film);
+ return filmStorage.create(film);
+ }
+
+ public Film update(Film film) {
+ validateFilm(film);
+ return filmStorage.update(film);
+ }
+
+ public Film getById(int id) {
+ return filmStorage.getById(id);
+ }
+
+ public List getAll() {
+ return filmStorage.getAll();
+ }
+
+ public void addLike(int filmId, int userId) {
+ Film film = filmStorage.getById(filmId);
+ userStorage.getById(userId); // Проверяем существование пользователя
+
+ film.getLikes().add(userId);
+ filmStorage.update(film);
+
+ log.info("Пользователь с id={} поставил лайк фильму с id={}", userId, filmId);
+ }
+
+ public void removeLike(int filmId, int userId) {
+ Film film = filmStorage.getById(filmId);
+
+ if (!film.getLikes().contains(userId)) {
+ throw new NotFoundException("Лайк от пользователя с id=" + userId + " не найден.");
+ }
+
+ film.getLikes().remove(userId);
+ filmStorage.update(film);
+
+ log.info("Пользователь с id={} удалил лайк фильму с id={}", userId, filmId);
+ }
+
+ public List getPopularFilms(int count) {
+ return filmStorage.getAll().stream()
+ .sorted(Comparator.comparingInt((Film f) -> f.getLikes().size()).reversed())
+ .limit(count)
+ .collect(Collectors.toList());
+ }
+
+ private void validateFilm(Film film) {
+ if (film.getName() == null || film.getName().isBlank()) {
+ throw new ValidationException("Название фильма не может быть пустым.");
+ }
+ if (film.getDescription() != null && film.getDescription().length() > 200) {
+ throw new ValidationException("Описание не должно превышать 200 символов.");
+ }
+ if (film.getReleaseDate() == null || film.getReleaseDate().isBefore(EARLIEST_RELEASE_DATE)) {
+ throw new ValidationException("Дата релиза не может быть раньше 28 декабря 1895 года.");
+ }
+ if (film.getDuration() <= 0) {
+ throw new ValidationException("Продолжительность фильма должна быть положительной.");
+ }
+ }
+}
\ 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..42f81ec
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java
@@ -0,0 +1,97 @@
+package ru.yandex.practicum.filmorate.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import ru.yandex.practicum.filmorate.model.User;
+import ru.yandex.practicum.filmorate.storage.UserStorage;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@Service
+@Slf4j
+public class UserService {
+ private final UserStorage userStorage;
+
+ @Autowired
+ public UserService(UserStorage userStorage) {
+ this.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) {
+ if (user.getName() == null || user.getName().isBlank()) {
+ user.setName(user.getLogin());
+ }
+ return userStorage.update(user);
+ }
+
+ public User getById(int id) {
+ return userStorage.getById(id);
+ }
+
+ public List getAll() {
+ return userStorage.getAll();
+ }
+
+ public void addFriend(int userId, int friendId) {
+ User user = userStorage.getById(userId);
+ User friend = userStorage.getById(friendId);
+
+ user.getFriends().add(friendId);
+ friend.getFriends().add(userId);
+
+ userStorage.update(user);
+ userStorage.update(friend);
+
+ log.info("Пользователь с id={} добавил в друзья пользователя с id={}", userId, friendId);
+ }
+
+ public void removeFriend(int userId, int friendId) {
+ User user = userStorage.getById(userId);
+ User friend = userStorage.getById(friendId);
+
+ user.getFriends().remove(friendId);
+ friend.getFriends().remove(userId);
+
+ userStorage.update(user);
+ userStorage.update(friend);
+
+ log.info("Пользователь с id={} удалил из друзей пользователя с id={}", userId, friendId);
+ }
+
+ public List getFriends(int userId) {
+ User user = userStorage.getById(userId);
+ List friends = new ArrayList<>();
+
+ for (Integer friendId : user.getFriends()) {
+ friends.add(userStorage.getById(friendId));
+ }
+
+ return friends;
+ }
+
+ public List getCommonFriends(int userId, int otherId) {
+ User user = userStorage.getById(userId);
+ User other = userStorage.getById(otherId);
+
+ Set commonFriendIds = new HashSet<>(user.getFriends());
+ commonFriendIds.retainAll(other.getFriends());
+
+ List commonFriends = new ArrayList<>();
+ for (Integer friendId : commonFriendIds) {
+ commonFriends.add(userStorage.getById(friendId));
+ }
+
+ return commonFriends;
+ }
+}
\ 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..8c4f56d
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java
@@ -0,0 +1,17 @@
+package ru.yandex.practicum.filmorate.storage;
+
+import ru.yandex.practicum.filmorate.model.Film;
+import java.util.List;
+
+public interface FilmStorage {
+
+ Film create(Film film);
+
+ Film update(Film film);
+
+ Film getById(int id);
+
+ List getAll();
+
+ void delete(int id);
+}
\ No newline at end of file
diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryFilmStorage.java
new file mode 100644
index 0000000..61c6774
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryFilmStorage.java
@@ -0,0 +1,58 @@
+package ru.yandex.practicum.filmorate.storage;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import ru.yandex.practicum.filmorate.exception.NotFoundException;
+import ru.yandex.practicum.filmorate.model.Film;
+import ru.yandex.practicum.filmorate.storage.FilmStorage;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Component
+@Slf4j
+public class InMemoryFilmStorage implements FilmStorage {
+ private final Map films = new HashMap<>();
+ private int nextId = 1;
+
+ @Override
+ public Film create(Film film) {
+ film.setId(nextId++);
+ films.put(film.getId(), film);
+ log.info("Фильм '{}' с id={} успешно создан.", film.getName(), film.getId());
+ return film;
+ }
+
+ @Override
+ public Film update(Film film) {
+ if (!films.containsKey(film.getId())) {
+ throw new NotFoundException("Фильм с id=" + film.getId() + " не найден.");
+ }
+ films.put(film.getId(), film);
+ log.info("Фильм '{}' с id={} успешно обновлен.", film.getName(), film.getId());
+ return film;
+ }
+
+ @Override
+ public Film getById(int id) {
+ Film film = films.get(id);
+ if (film == null) {
+ throw new NotFoundException("Фильм с id=" + id + " не найден.");
+ }
+ return film;
+ }
+
+ @Override
+ public List getAll() {
+ return new ArrayList<>(films.values());
+ }
+
+ @Override
+ public void delete(int id) {
+ if (!films.containsKey(id)) {
+ throw new NotFoundException("Фильм с id=" + id + " не найден.");
+ }
+ films.remove(id);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryUserStorage.java
new file mode 100644
index 0000000..403d2c8
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryUserStorage.java
@@ -0,0 +1,58 @@
+package ru.yandex.practicum.filmorate.storage.user;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import ru.yandex.practicum.filmorate.exception.NotFoundException;
+import ru.yandex.practicum.filmorate.model.User;
+import ru.yandex.practicum.filmorate.storage.UserStorage;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Component
+@Slf4j
+public class InMemoryUserStorage implements UserStorage {
+ private final Map users = new HashMap<>();
+ private int nextId = 1;
+
+ @Override
+ public User create(User user) {
+ user.setId(nextId++);
+ users.put(user.getId(), user);
+ log.info("Пользователь '{}' с id={} успешно создан.", user.getLogin(), user.getId());
+ return user;
+ }
+
+ @Override
+ public User update(User user) {
+ if (!users.containsKey(user.getId())) {
+ throw new NotFoundException("Пользователь с id=" + user.getId() + " не найден.");
+ }
+ users.put(user.getId(), user);
+ log.info("Пользователь '{}' с id={} успешно обновлен.", user.getLogin(), user.getId());
+ return user;
+ }
+
+ @Override
+ public User getById(int id) {
+ User user = users.get(id);
+ if (user == null) {
+ throw new NotFoundException("Пользователь с id=" + id + " не найден.");
+ }
+ return user;
+ }
+
+ @Override
+ public List getAll() {
+ return new ArrayList<>(users.values());
+ }
+
+ @Override
+ public void delete(int id) {
+ if (!users.containsKey(id)) {
+ throw new NotFoundException("Пользователь с id=" + id + " не найден.");
+ }
+ users.remove(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..b34fd80
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java
@@ -0,0 +1,17 @@
+package ru.yandex.practicum.filmorate.storage;
+
+import ru.yandex.practicum.filmorate.model.User;
+import java.util.List;
+
+public interface UserStorage {
+
+ User create(User user);
+
+ User update(User user);
+
+ User getById(int id);
+
+ List getAll();
+
+ void delete(int id);
+}
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 8b13789..636895c 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1 +1,2 @@
-
+# Logbook configuration
+logging.level.org.zalando.logbook=TRACE
diff --git a/src/test/java/ru/yandex/practicum/filmorate/FilmControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/FilmControllerTest.java
new file mode 100644
index 0000000..63c6d6e
--- /dev/null
+++ b/src/test/java/ru/yandex/practicum/filmorate/FilmControllerTest.java
@@ -0,0 +1,122 @@
+package ru.yandex.practicum.filmorate;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import ru.yandex.practicum.filmorate.controller.FilmController;
+import ru.yandex.practicum.filmorate.model.Film;
+import ru.yandex.practicum.filmorate.service.FilmService;
+
+import java.time.LocalDate;
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@WebMvcTest(FilmController.class)
+class FilmControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @MockBean
+ private FilmService filmService;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ private Film validFilm;
+
+ @BeforeEach
+ void setUp() {
+ validFilm = new Film();
+ validFilm.setId(1);
+ validFilm.setName("Test Film");
+ validFilm.setDescription("Test Description");
+ validFilm.setReleaseDate(LocalDate.of(2000, 1, 1));
+ validFilm.setDuration(120);
+ }
+
+ @Test
+ void shouldCreateFilm() throws Exception {
+ when(filmService.create(any(Film.class))).thenReturn(validFilm);
+
+ mockMvc.perform(post("/films")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(validFilm)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id").value(1))
+ .andExpect(jsonPath("$.name").value("Test Film"))
+ .andExpect(jsonPath("$.duration").value(120));
+ }
+
+ @Test
+ void shouldGetAllFilms() throws Exception {
+ when(filmService.getAll()).thenReturn(List.of(validFilm));
+
+ mockMvc.perform(get("/films"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$[0].id").value(1))
+ .andExpect(jsonPath("$[0].name").value("Test Film"));
+ }
+
+ @Test
+ void shouldGetFilmById() throws Exception {
+ when(filmService.getById(1)).thenReturn(validFilm);
+
+ mockMvc.perform(get("/films/1"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id").value(1))
+ .andExpect(jsonPath("$.name").value("Test Film"));
+ }
+
+ @Test
+ void shouldUpdateFilm() throws Exception {
+ when(filmService.update(any(Film.class))).thenReturn(validFilm);
+
+ mockMvc.perform(put("/films")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(validFilm)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id").value(1))
+ .andExpect(jsonPath("$.name").value("Test Film"));
+ }
+
+ @Test
+ void shouldAddLike() throws Exception {
+ mockMvc.perform(put("/films/1/like/2"))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ void shouldRemoveLike() throws Exception {
+ mockMvc.perform(delete("/films/1/like/2"))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ void shouldGetPopularFilms() throws Exception {
+ when(filmService.getPopularFilms(anyInt())).thenReturn(List.of(validFilm));
+
+ mockMvc.perform(get("/films/popular?count=10"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$[0].id").value(1));
+ }
+
+ @Test
+ void shouldGetPopularFilmsWithDefaultCount() throws Exception {
+ when(filmService.getPopularFilms(10)).thenReturn(List.of(validFilm));
+
+ mockMvc.perform(get("/films/popular"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$[0].id").value(1));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/yandex/practicum/filmorate/UserControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/UserControllerTest.java
new file mode 100644
index 0000000..1fd378d
--- /dev/null
+++ b/src/test/java/ru/yandex/practicum/filmorate/UserControllerTest.java
@@ -0,0 +1,122 @@
+package ru.yandex.practicum.filmorate;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import ru.yandex.practicum.filmorate.controller.UserController;
+import ru.yandex.practicum.filmorate.model.User;
+import ru.yandex.practicum.filmorate.service.UserService;
+
+import java.time.LocalDate;
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@WebMvcTest(UserController.class)
+class UserControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @MockBean
+ private UserService userService;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ private User validUser;
+
+ @BeforeEach
+ void setUp() {
+ validUser = new User();
+ validUser.setId(1);
+ validUser.setEmail("test@example.com");
+ validUser.setLogin("testLogin");
+ validUser.setName("Test Name");
+ validUser.setBirthday(LocalDate.of(1990, 1, 1));
+ }
+
+ @Test
+ void shouldCreateUser() throws Exception {
+ when(userService.create(any(User.class))).thenReturn(validUser);
+
+ mockMvc.perform(post("/users")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(validUser)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id").value(1))
+ .andExpect(jsonPath("$.email").value("test@example.com"))
+ .andExpect(jsonPath("$.login").value("testLogin"));
+ }
+
+ @Test
+ void shouldGetAllUsers() throws Exception {
+ when(userService.getAll()).thenReturn(List.of(validUser));
+
+ mockMvc.perform(get("/users"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$[0].id").value(1))
+ .andExpect(jsonPath("$[0].email").value("test@example.com"));
+ }
+
+ @Test
+ void shouldGetUserById() throws Exception {
+ when(userService.getById(1)).thenReturn(validUser);
+
+ mockMvc.perform(get("/users/1"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id").value(1))
+ .andExpect(jsonPath("$.email").value("test@example.com"));
+ }
+
+ @Test
+ void shouldUpdateUser() throws Exception {
+ when(userService.update(any(User.class))).thenReturn(validUser);
+
+ mockMvc.perform(put("/users")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(validUser)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id").value(1))
+ .andExpect(jsonPath("$.email").value("test@example.com"));
+ }
+
+ @Test
+ void shouldAddFriend() throws Exception {
+ mockMvc.perform(put("/users/1/friends/2"))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ void shouldRemoveFriend() throws Exception {
+ mockMvc.perform(delete("/users/1/friends/2"))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ void shouldGetFriends() throws Exception {
+ when(userService.getFriends(anyInt())).thenReturn(List.of(validUser));
+
+ mockMvc.perform(get("/users/1/friends"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$[0].id").value(1));
+ }
+
+ @Test
+ void shouldGetCommonFriends() throws Exception {
+ when(userService.getCommonFriends(anyInt(), anyInt())).thenReturn(List.of(validUser));
+
+ mockMvc.perform(get("/users/1/friends/common/2"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$[0].id").value(1));
+ }
+}
\ No newline at end of file