From a8611f2e900aaf6b08c985ef91b38050a168897d Mon Sep 17 00:00:00 2001 From: Crodi Date: Sat, 25 Oct 2025 20:30:49 +0300 Subject: [PATCH 01/35] fix: deleted ValidBirthday, BirthdayValidator and BirthdayValidatorTest. Changed it to @PastOrPresent in User --- src/main/java/ru/yandex/practicum/filmorate/model/User.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/User.java b/src/main/java/ru/yandex/practicum/filmorate/model/User.java index ac664a5..e48cf03 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/User.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java @@ -2,10 +2,10 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.PastOrPresent; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import ru.yandex.practicum.filmorate.validation.ValidBirthday; import ru.yandex.practicum.filmorate.validation.ValidLogin; import java.time.LocalDate; @@ -39,6 +39,6 @@ public class User { private String name; - @ValidBirthday + @PastOrPresent private LocalDate birthday; } From e810dead1e916262fd9e5e41eb157122f61432ef Mon Sep 17 00:00:00 2001 From: Crodi Date: Mon, 27 Oct 2025 02:42:15 +0300 Subject: [PATCH 02/35] fix: typo --- src/main/java/ru/yandex/practicum/filmorate/model/Film.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ada7009..df189bb 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -11,8 +11,8 @@ import java.time.LocalDate; /** - * User. - *

DTO to represent user + * Film. + *

DTO to represent film * *

Properties: *

*/ @Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) @NoArgsConstructor @AllArgsConstructor -public class User { - - private Long id; +public class User extends StorageData { + + public User(Long id, String email, String login, String name, LocalDate birthday) { + super(id); + this.email = email; + this.login = login; + this.name = name; + this.birthday = birthday; + } @NotBlank(message = "Почта не может быть пустой") @Email From bb147bb0ccdc4bc912ef7bac63d24401920e1cdc Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 29 Oct 2025 23:17:03 +0300 Subject: [PATCH 08/35] feat: Add inheritance from StorageData. Add new read-only field "likes". --- .../practicum/filmorate/model/Film.java | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) 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 df189bb..c85ae82 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -1,15 +1,15 @@ package ru.yandex.practicum.filmorate.model; +import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; import ru.yandex.practicum.filmorate.validation.ValidReleaseDate; import java.time.LocalDate; + /** * Film. *

DTO to represent film @@ -21,14 +21,23 @@ *

  • description - Film's description, must not be over 200 characters
  • *
  • releaseDate - Film`s release date, must be after 1985-01-28
  • *
  • duration - Film's duration, must be positive
  • + *
  • likes - Film's likes, initial value is 0, read only
  • * */ + @Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) @NoArgsConstructor -@AllArgsConstructor -public class Film { +public class Film extends StorageData { - private Long id; + public Film(Long id, String name, String description, LocalDate releaseDate, Integer duration) { + super(id); + this.name = name; + this.description = description; + this.releaseDate = releaseDate; + this.duration = duration; + } @NotBlank(message = "Название не может быть пустым") private String name; @@ -41,4 +50,7 @@ public class Film { @Positive private Integer duration; + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private Long likes = 0L; } From d8597ee5dd1dd55af832fa8a440ca4a67b9bb8af Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 29 Oct 2025 23:23:58 +0300 Subject: [PATCH 09/35] feat: Add interface to represent Storage. Basic CRUD operations. --- .../filmorate/storage/BasicStorage.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/BasicStorage.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/BasicStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/BasicStorage.java new file mode 100644 index 0000000..c327f8b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/BasicStorage.java @@ -0,0 +1,26 @@ +package ru.yandex.practicum.filmorate.storage; + +import ru.yandex.practicum.filmorate.model.StorageData; + +import java.util.Collection; + +public interface BasicStorage { + + T add(T t); + + T update(T t); + + T remove(Long id); + + void clear(); + + T get(Long id); + + Collection getAll(); + + boolean contains(Long id); + + int size(); + + void throwIfNotFound(Long id); +} From 0f262c0af02d62edc2e0240c6409900ae6a7ab4e Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 29 Oct 2025 23:24:42 +0300 Subject: [PATCH 10/35] feat: Add interfaces with special methods to User and Film. --- .../filmorate/storage/film/FilmStorage.java | 15 +++++++++++++++ .../filmorate/storage/user/UserStorage.java | 17 +++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java new file mode 100644 index 0000000..4224c22 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java @@ -0,0 +1,15 @@ +package ru.yandex.practicum.filmorate.storage.film; + +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.storage.BasicStorage; + +import java.util.Collection; + +public interface FilmStorage extends BasicStorage { + + Film addLike(Long filmId, Long userId); + + Film removeLike(Long filmId, Long userId); + + Collection getTopFilms(Long count); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java new file mode 100644 index 0000000..f72cde7 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java @@ -0,0 +1,17 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.BasicStorage; + +import java.util.Collection; + +public interface UserStorage extends BasicStorage { + + Collection addFriend(Long senderId, Long receiverId); + + void deleteFriend(Long senderId, Long receiverId); + + Collection getFriends(Long id); + + Collection getCommonFriends(Long id, Long otherId); +} From 07fb18bc18a2a1b21930f30b8a3326d78b3c9c2d Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 29 Oct 2025 23:25:43 +0300 Subject: [PATCH 11/35] feat: Add abstract class AbstractStorage. Implements BasicStorage --- .../filmorate/storage/AbstractStorage.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/AbstractStorage.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/AbstractStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/AbstractStorage.java new file mode 100644 index 0000000..d489d99 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/AbstractStorage.java @@ -0,0 +1,79 @@ +package ru.yandex.practicum.filmorate.storage; + +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.StorageData; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +public abstract class AbstractStorage implements BasicStorage { + + private final Map storage; + private final AtomicLong idGenerator; + + public AbstractStorage() { + this.storage = new ConcurrentHashMap<>(); + idGenerator = new AtomicLong(1); + } + + @Override + public T add(T value) { + value.setId(idGenerator.getAndIncrement()); + storage.put(value.getId(), value); + + return storage.get(value.getId()); + } + + @Override + public T update(T newUser) { + throwIfNotFound(newUser.getId()); + + storage.put(newUser.getId(), newUser); + + return storage.get(newUser.getId()); + } + + @Override + public T remove(Long id) { + throwIfNotFound(id); + + return storage.remove(id); + } + + @Override + public void clear() { + storage.clear(); + } + + @Override + public T get(Long id) { + throwIfNotFound(id); + + return storage.get(id); + } + + @Override + public Collection getAll() { + return List.copyOf(storage.values()); + } + + @Override + public boolean contains(Long id) { + return storage.containsKey(id); + } + + @Override + public int size() { + return storage.size(); + } + + @Override + public void throwIfNotFound(Long id) { + if (!contains(id)) { + throw new NotFoundException("Сущность с id = " + id + " не найдена"); + } + } +} From d121baf2c751c484e17a60b0c4f3174445858c6f Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 29 Oct 2025 23:26:54 +0300 Subject: [PATCH 12/35] feat: Add InMemoryUserStorage. Implements UserStorage. Extends AbstractStorage. --- .../storage/user/InMemoryUserStorage.java | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java new file mode 100644 index 0000000..5a17864 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java @@ -0,0 +1,103 @@ +package ru.yandex.practicum.filmorate.storage.user; + +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.AbstractStorage; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * In-memory implementation of user storage. + *

    + * Provides thread-safe storage for User objects using ConcurrentHashMap with atomic ID generation. + * Suitable for development and testing environments without persistent storage requirements. + *

    + * + * @see User + * @see UserStorage + */ +@Component +public class InMemoryUserStorage extends AbstractStorage implements UserStorage { + + private final Map> friendships; + + public InMemoryUserStorage() { + super(); + friendships = new ConcurrentHashMap<>(); + } + + @Override + public User add(User user) { + User returnUser = super.add(user); + friendships.put(user.getId(), new ConcurrentHashMap<>()); + + return returnUser; + } + + @Override + public User remove(Long id) { + throwIfNotFound(id); + + Set friendIds = friendships.get(id).keySet(); + friendIds.forEach(friendId -> friendships.get(friendId).remove(id)); + friendships.remove(id); + + return super.remove(id); + } + + @Override + public void clear() { + super.clear(); + friendships.clear(); + } + + @Override + public void throwIfNotFound(Long id) { + if (!contains(id)) { + throw new NotFoundException("Пользователь с id = " + id + " не найден"); + } + } + + @Override + public Collection addFriend(Long senderId, Long receiverId) { + User receiver = get(receiverId); + User friend = get(senderId); + + friendships.get(receiverId).put(senderId, friend); + friendships.get(senderId).put(receiverId, receiver); + return friendships.get(senderId).values(); + } + + @Override + public void deleteFriend(Long senderId, Long receiverId) { + throwIfNotFound(senderId); + throwIfNotFound(receiverId); + + friendships.get(senderId).remove(receiverId); + friendships.get(receiverId).remove(senderId); + } + + @Override + public Collection getFriends(Long id) { + throwIfNotFound(id); + return friendships.get(id).values(); + } + + @Override + public Collection getCommonFriends(Long id, Long otherId) { + throwIfNotFound(id); + throwIfNotFound(otherId); + + Set friends = friendships.get(id).keySet(); + Map otherFriends = friendships.get(otherId); + + return friends.stream() + .filter(otherFriends::containsKey) + .map(otherFriends::get) + .toList(); + } +} \ No newline at end of file From f3695ccd98e77d32c18a2af677af2c6d22aa6224 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 29 Oct 2025 23:27:07 +0300 Subject: [PATCH 13/35] feat: Add InMemoryFilmStorage. Implements FilmStorage. Extends AbstractStorage. --- .../storage/film/InMemoryFilmStorage.java | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java new file mode 100644 index 0000000..cf44e38 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java @@ -0,0 +1,89 @@ +package ru.yandex.practicum.filmorate.storage.film; + +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.AbstractStorage; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * In-memory implementation of film storage. + *

    + * Provides thread-safe storage for Film objects using ConcurrentHashMap with atomic ID generation. + * Suitable for development and testing environments without persistent storage requirements. + *

    + * + * @see Film + * @see FilmStorage + */ +@Component +public class InMemoryFilmStorage extends AbstractStorage implements FilmStorage { + + private final Map> likes; + + public InMemoryFilmStorage() { + super(); + likes = new ConcurrentHashMap<>(); + } + + @Override + public Film add(Film film) { + Film returnFilm = super.add(film); + likes.put(film.getId(), new HashSet<>()); + + return returnFilm; + } + + @Override + public Film remove(Long id) { + Film film = super.remove(id); + likes.remove(film.getId()); + + return film; + } + + @Override + public void clear() { + super.clear(); + likes.clear(); + } + + @Override + public void throwIfNotFound(Long id) { + if (!contains(id)) { + throw new NotFoundException("Фильм с id = " + id + " не найден"); + } + } + + @Override + public Film addLike(Long filmId, Long userId) { + Film film = get(filmId); + + Set filmLikes = likes.get(filmId); + filmLikes.add(userId); + film.setLikes((long) filmLikes.size()); + + return film; + } + + @Override + public Film removeLike(Long filmId, Long userId) { + Film film = get(filmId); + + Set set = likes.get(filmId); + set.remove(userId); + film.setLikes((long) set.size()); + + return film; + } + + @Override + public Collection getTopFilms(Long count) { + return getAll().stream() + .sorted(Comparator.comparingLong(Film::getLikes).reversed()) + .limit(count) + .toList(); + } +} From 2c61d03b6e7fae423ed5a9c45cf83da82bc553e7 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 29 Oct 2025 23:28:00 +0300 Subject: [PATCH 14/35] feat: Add new ExceptionHandler. Refactored code for DRY principle. --- .../exception/GlobalExceptionHandler.java | 126 ++++++++++-------- 1 file changed, 72 insertions(+), 54 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/GlobalExceptionHandler.java b/src/main/java/ru/yandex/practicum/filmorate/exception/GlobalExceptionHandler.java index b0d53b7..42ce9ce 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/exception/GlobalExceptionHandler.java +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/GlobalExceptionHandler.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.exc.InvalidFormatException; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -11,6 +12,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import java.util.HashMap; import java.util.Map; @@ -32,23 +34,16 @@ public ResponseEntity handleValidationExceptions( MethodArgumentNotValidException ex, HttpServletRequest request) { - log.warn("Resolved: [{}]", ex.getClass().getName()); - log.debug("Validation error", ex); + logInfo(ex, "Validation error from handleHttpMessageNotReadable"); Map errors = new HashMap<>(); - ex.getBindingResult().getAllErrors().forEach((error) -> { String fieldName = ((FieldError) error).getField(); String errorMessage = error.getDefaultMessage(); errors.put(fieldName, errorMessage); }); - return ResponseEntity.badRequest().body( - new ApiError("Bad Request", - HttpStatus.BAD_REQUEST.value(), - request.getRequestURI(), - errors) - ); + return createBadRequest(request.getRequestURI(), errors); } /** @@ -60,25 +55,60 @@ public ResponseEntity handleHttpMessageNotReadable( HttpMessageNotReadableException ex, HttpServletRequest request) { - log.warn("Resolved: [{}]", ex.getClass().getName()); - log.debug("Invalid request format", ex); + logInfo(ex, "Invalid request format from handleHttpMessageNotReadable"); + return createBadRequest(request.getRequestURI(), getErrors(ex)); + } - return ResponseEntity.badRequest().body( - new ApiError("Bad Request", - HttpStatus.BAD_REQUEST.value(), - request.getRequestURI(), - getErrors(ex) - ) - ); + /** + * Handles business logic "not found" scenarios. + * Returns 404 status with descriptive message. + */ + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFoundException( + NotFoundException ex, + HttpServletRequest request) { + + logInfo(ex, "Not Found from handleNotFoundException"); + + return createResponseEntity( + HttpStatus.NOT_FOUND, + request.getRequestURI(), + Map.of("error", ex.getMessage())); + } + + /** + * Handles custom validation exceptions from service layer. + * Returns 400 status with business rule violation details. + */ + @ExceptionHandler(ValidationException.class) + public ResponseEntity handleValidationException( + ValidationException ex, + HttpServletRequest request) { + + logInfo(ex, "Validation Error from handleValidationException"); + return createBadRequest(request.getRequestURI(), Map.of("error", ex.getMessage())); + } + + /** + * Handles custom validation exceptions from service layer. + * Returns 400 status without details dut to security reasons. + */ + @ExceptionHandler({MethodArgumentTypeMismatchException.class, ConstraintViolationException.class}) + public ResponseEntity handleMismatchAndConstraintViolation( + Exception ex, + HttpServletRequest request) { + + logInfo(ex, "Bad Request Error from handleMismatchAndConstraintViolation"); + return createBadRequest(request.getRequestURI(), Map.of("error", "Invalid request format")); } /** * Helper for HttpMessageNotReadableException handler - * Extracts meaningful error messages from JSON parsing exceptions. + *

    Extracts meaningful error messages from JSON parsing exceptions. * - * @return map with error + * @return map with errors */ - private static Map getErrors(HttpMessageNotReadableException ex) { + private Map getErrors(HttpMessageNotReadableException ex) { String message = "Invalid request format"; if (ex.getCause() instanceof JsonParseException jpe) { @@ -95,46 +125,34 @@ private static Map getErrors(HttpMessageNotReadableException ex) } /** - * Handles business logic "not found" scenarios. - * Returns 404 status with descriptive message. + * Helper method to construct a ResponseEntity with ApiError as body */ - @ExceptionHandler(NotFoundException.class) - public ResponseEntity handleNotFoundException( - NotFoundException ex, - HttpServletRequest request) { - - log.warn("Resolved: [{}]", ex.getClass().getName()); - log.debug("Not Found", ex); - Map errors = new HashMap<>(); - - return ResponseEntity.status(HttpStatus.NOT_FOUND).body( - new ApiError("Not Found", - HttpStatus.NOT_FOUND.value(), - request.getRequestURI(), - Map.of("error", ex.getMessage()) + private ResponseEntity createResponseEntity(HttpStatus status, String URI, Map errors) { + return ResponseEntity.status(status).body( + new ApiError( + status.getReasonPhrase(), + status.value(), + URI, + errors ) ); } /** - * Handles custom validation exceptions from service layer. - * Returns 400 status with business rule violation details. + * Helper method to construct a BadRequest ResponseEntity. */ - @ExceptionHandler(ValidationException.class) - public ResponseEntity handleValidationException( - ValidationException ex, - HttpServletRequest request) { - - log.warn("Resolved: [{}]", ex.getClass().getName()); - log.debug("Validation Error", ex); - - return ResponseEntity.badRequest().body( - new ApiError("Bad Request", - HttpStatus.BAD_REQUEST.value(), - request.getRequestURI(), - Map.of("error", ex.getMessage()) + private ResponseEntity createBadRequest(String path, Map errors) { + return createResponseEntity( + HttpStatus.BAD_REQUEST, + path, + errors); + } - ) - ); + /** + * Helper method to log info. + */ + private void logInfo(Throwable ex, String info) { + log.info("Resolved: [{}] Info: [{}]", ex.getClass().getName(), info); + log.debug(info, ex); } } From a81d56593610d5c0f5d55041a8486414250b0c27 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 29 Oct 2025 23:29:00 +0300 Subject: [PATCH 15/35] feat: Add FriendShipService. Works with storage and controls al operations with friends. --- .../service/user/FriendShipService.java | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/user/FriendShipService.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/user/FriendShipService.java b/src/main/java/ru/yandex/practicum/filmorate/service/user/FriendShipService.java new file mode 100644 index 0000000..158aaf4 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/user/FriendShipService.java @@ -0,0 +1,89 @@ +package ru.yandex.practicum.filmorate.service.user; + +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.ValidationException; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; + +import java.util.Collection; +import java.util.Objects; + +/** + * Service for managing user friendships and social connections. + *

    + * Handles friendship operations including adding/removing friends, retrieving friend lists, + * and finding common friends between users. Maintains bidirectional friendship relationships. + *

    + * + * @see User + * @see UserStorage + */ +@Service +public class FriendShipService { + + private final UserStorage userStorage; + + /** + * Constructor for dependency injection + */ + public FriendShipService(UserStorage userStorage) { + this.userStorage = userStorage; + } + + /** + * Makes connection(friendship) between two users. + *
  • Sender adds Receiver to his friend map. + *
  • Receiver adds Sender to his friend map. + * + * @param senderId ID of the user that sends friendship request. Must be positive and exist in the system. + * @param receiverId ID of the user that gets friendship request. Must be positive and exist in the system. + * @return senderId`s collection of friends + * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user is not found + * @throws ru.yandex.practicum.filmorate.exception.ValidationException if senderId equals receiverId + */ + public Collection addFriend(Long senderId, Long receiverId) { + if (Objects.equals(receiverId, senderId)) { + throw new ValidationException("Сам себя не добавишь - никто не добавит"); + } + + return userStorage.addFriend(senderId, receiverId); + } + + /** + * Breaks connection(friendship) between two users. + *
  • Sender removes Receiver to his friend map. + *
  • Receiver removes Sender to his friend map. + * + * @param senderId ID of the user from where friend should be deleted. Must be positive and exist in the system. + * @param receiverId ID of the user that is being deleted. Must be positive and exist in the system. + * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user is not found + */ + public void deleteFriend(Long senderId, Long receiverId) { + userStorage.deleteFriend(senderId, receiverId); + } + + /** + * Returns user`s friends. + * + * @param id ID of the user to get friends. Must be positive and exist in the system. + * @return Users`s collection of friends + * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user is not found + */ + public Collection getFriends(Long id) { + return userStorage.getFriends(id); + } + + /** + * Finds intersection of friends between two users. + *

    Example: If user1 has friends [A, B, C] and user2 has friends [B, C, D], + * returns [B, C]. + * + * @param id first user ID. Must be positive and exist in the system. + * @param otherId second user ID. Must be positive and exist in the system. + * @return common friends collection. + * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user is not found + */ + public Collection getCommonFriends(Long id, Long otherId) { + return userStorage.getCommonFriends(id, otherId); + } +} From 3db7ff6449c6ecd1a04198ff0984b8b2c3b64ab3 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 29 Oct 2025 23:29:28 +0300 Subject: [PATCH 16/35] feat: Add UserService. Works with storage and controls all operations with storage. Main Service for User. --- .../filmorate/service/user/UserService.java | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java new file mode 100644 index 0000000..480569b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java @@ -0,0 +1,130 @@ +package ru.yandex.practicum.filmorate.service.user; + +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; + +import java.util.Collection; + +/** + * Main service for user operations and business logic. + *

    + * Provides comprehensive user management functionality including CRUD operations, friendship management, + * and social features. Delegates friendship-specific operations to FriendShipService. + *

    + * + * @see User + * @see UserStorage + * @see FriendShipService + */ +@Service +public class UserService { + + private final UserStorage userStorage; + private final FriendShipService friendShipService; + + /** + * Constructor for dependency injection + */ + public UserService(UserStorage userStorage, FriendShipService friendShipService) { + this.userStorage = userStorage; + this.friendShipService = friendShipService; + } + + /** + * @param id ID of the user to be deleted. Must be positive and exist in the system. + * @return User from storage. + * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user is not found. + */ + public User getUser(Long id) { + return userStorage.get(id); + } + + /** + * @return Collection of all users in storage. + */ + public Collection getAllUsers() { + return userStorage.getAll(); + } + + /** + * Add user to the storage. + *

    If user`s name is null, then login is set as a name. + * + * @param user User to create. + * @return created User. + */ + public User addUser(User user) { + + if (user.getName() == null || user.getName().isBlank()) { + user.setName(user.getLogin()); + } + + return userStorage.add(user); + } + + /** + * Updates user in the storage. + * + * @param user User to replace. User`s id must be in the system. + * @return updated User. + * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user is not found + */ + public User updateUser(User user) { + return userStorage.update(user); + } + + /** + * Deletes user from the storage. + * + * @param id ID of the user to be deleted. Must be positive and exist in the system. + */ + public void deleteUser(Long id) { + userStorage.remove(id); + } + + /** + * Create friendship between users. Only one side needs to confirm friendship. + * + * @param senderId ID of the user that sends friendship request. Must be positive and exist in the system. + * @param receiverId ID of the user that gets friendship request. Must be positive and exist in the system. + * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user is not found + * @throws ru.yandex.practicum.filmorate.exception.ValidationException if senderId equals receiverId + */ + public Collection addFriend(Long senderId, Long receiverId) { + return friendShipService.addFriend(senderId, receiverId); + } + + /** + * Deletes friendship between users without. Only one side needs to confirm deletion of friendship. + * FriendShip stops if any of users breaks it. + * + * @param senderId ID of the user from where friend should be deleted. Must be positive and exist in the system. + * @param receiverId ID of the user that is being deleted. Must be positive and exist in the system. + * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user is not found + */ + public void deleteFriend(Long senderId, Long receiverId) { + friendShipService.deleteFriend(senderId, receiverId); + } + + /** + * @param id ID of the user to get friends. Must be positive and exist in the system. + * @return Collection of all user`s friends. + * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user is not found + */ + public Collection getFriends(Long id) { + return friendShipService.getFriends(id); + } + + /** + * Searches for common friends between two users. + * + * @param id ID of the first user. Must be positive and exist in the system. + * @param otherId ID of the second user. Must be positive and exist in the system. + * @return Collection of all common friends between users. + * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user is not found + */ + public Collection getCommonFriends(Long id, Long otherId) { + return friendShipService.getCommonFriends(id, otherId); + } +} From c06faecc8048c327b67c487906178dc2f8cd54ee Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 29 Oct 2025 23:29:55 +0300 Subject: [PATCH 17/35] feat: Add FilmLikeService. Works with storage and controls all operations with likes. --- .../service/film/FilmLikeService.java | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/film/FilmLikeService.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmLikeService.java b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmLikeService.java new file mode 100644 index 0000000..c50a6af --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmLikeService.java @@ -0,0 +1,77 @@ +package ru.yandex.practicum.filmorate.service.film; + +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.storage.film.FilmStorage; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; + +import java.util.Collection; + +/** + * Service for managing film likes functionality. + *

    + * Handles operations related to film likes including adding, removing likes and retrieving top-liked films. + * Maintains an internal concurrent storage for tracking user likes for each film. + *

    + * + * @see Film + * @see FilmStorage + * @see UserStorage + */ +@Service +public class FilmLikeService { + + private final FilmStorage filmStorage; + private final UserStorage userStorage; + + /** + * Constructor for dependency injection + */ + public FilmLikeService(FilmStorage filmStorage, UserStorage userStorage) { + this.filmStorage = filmStorage; + this.userStorage = userStorage; + } + + /** + * Adds like to film. + *

    Adds userId in set of ids for specific film. + * Sets likes value in Film to set size. + * + * @param filmId film`s id to find set of likes. + * @param userId user`s id to add to set of likes. + * @return Film where like was added. + * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user or films is not present in storage. + */ + public Film addLike(Long filmId, Long userId) { + userStorage.throwIfNotFound(userId); + + return filmStorage.addLike(filmId, userId); + } + + /** + * Remove like from film. + *

    Remove userId from set of ids for specific film. + * Sets likes value in Film to set size. + * + * @param filmId film`s id to find set of likes. + * @param userId user`s id to delete from set of likes. + * @return Film where like was deleted. + * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user or films is not present in storage. + */ + public Film removeLike(Long filmId, Long userId) { + userStorage.throwIfNotFound(userId); + + return filmStorage.removeLike(filmId, userId); + } + + /** + * Return top-liked films with limit. + *

    Streams through all films and sorts them by their likes amount. + * + * @param count limit to returned collection. + * @return collection of Films. + */ + public Collection getTopFilms(Long count) { + return filmStorage.getTopFilms(count); + } +} From cbe1aa4978518968122edeb667748da7e2983746 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 29 Oct 2025 23:30:23 +0300 Subject: [PATCH 18/35] feat: Add FilmService. Works with storage and controls all operations with storage. Main Service for Film. --- .../filmorate/service/film/FilmService.java | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java new file mode 100644 index 0000000..f13d1aa --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java @@ -0,0 +1,116 @@ +package ru.yandex.practicum.filmorate.service.film; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.storage.film.FilmStorage; + +import java.util.Collection; + +/** + * Main service for film operations and business logic. + *

    + * Provides comprehensive film management functionality including CRUD operations, likes management, + * and popular films retrieval. Delegates likes-specific operations to FilmLikeService. + *

    + * + * @see Film + * @see FilmStorage + * @see FilmLikeService + */ +@Service +@Slf4j +public class FilmService { + + private final FilmStorage filmStorage; + private final FilmLikeService filmLikeService; + + /** + * Constructor for dependency injection + */ + public FilmService(FilmStorage filmStorage, FilmLikeService filmLikeService) { + this.filmStorage = filmStorage; + this.filmLikeService = filmLikeService; + } + + /** + * Retrieves specific film from the storage. + * + * @param id Film`s id to get. + * @return Film. + */ + public Film getFilm(Long id) { + return filmStorage.get(id); + } + + /** + * Retrieves all filmsStorage from the storage. + * + * @return Collection of all filmsStorage. + */ + public Collection getAllFilms() { + return filmStorage.getAll(); + } + + /** + * Adds film to the storage. + * + * @param film Film to add. + * @return created Film. + */ + public Film addFilm(Film film) { + return filmStorage.add(film); + } + + /** + * Updates film in the storage. + * + * @param film Film to update. + * @return updated Film. + */ + public Film updateFilm(Film film) { + return filmStorage.update(film); + } + + /** + * Deletes film from the storage. + * + * @param id Film`s id to delete. + */ + public void deleteFilm(Long id) { + filmStorage.remove(id); + } + + /** + * Adds like to the film. + * + * @param filmId Film`s id to add like. + * @param userId User`s id to set like. + * @return Film where likes where added. + */ + public Film addLike(Long filmId, Long userId) { + return filmLikeService.addLike(filmId, userId); + } + + /** + * Deletes like from the film. + * + * @param filmId Film`s id to delete like. + * @param userId User`s id to delete like. + * @return Film where likes where deleted. + */ + public Film removeLike(Long filmId, Long userId) { + return filmLikeService.removeLike(filmId, userId); + } + + /** + * Return top ranked films. + * + * @param count limit to collection. + * @return collection of Films. + */ + public Collection getTopFilms(Long count) { + return filmLikeService.getTopFilms(count); + } + +} From a9a419faa190dfd4c34ee5f0866a2aa2e8511671 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 29 Oct 2025 23:32:46 +0300 Subject: [PATCH 19/35] feat: Completely reworked Controller. Deleted everything that controller should not have. All operations go through UserService. Add new endpoints. --- .../filmorate/controller/UserController.java | 96 +++++++++++++------ 1 file changed, 68 insertions(+), 28 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java index ba11211..dbcd635 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -1,29 +1,29 @@ package ru.yandex.practicum.filmorate.controller; import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.service.user.UserService; import java.util.Collection; -import java.util.HashMap; -import java.util.concurrent.atomic.AtomicLong; @RestController @RequestMapping("/users") @Slf4j public class UserController { - private final HashMap users = new HashMap<>(); - private final AtomicLong idGenerator = new AtomicLong(); + private final UserService userService; /** - * Private constructor to initialize ID generator with starting ID = 1. + * Constructor for dependency injection */ - private UserController() { - idGenerator.set(1); + public UserController(UserService userService) { + this.userService = userService; } /** @@ -34,8 +34,7 @@ private UserController() { */ @GetMapping public Collection getUsers() { - log.info("GET /users - returning {} users", users.size()); - return users.values(); + return userService.getAllUsers(); } /** @@ -46,17 +45,7 @@ public Collection getUsers() { */ @PostMapping public User addUser(@Valid @RequestBody User user) { - - user.setId(idGenerator.getAndIncrement()); - - if (user.getName() == null) { - user.setName(user.getLogin()); - } - - log.info("POST /users - User created: {}", user); - users.put(user.getId(), user); - - return user; + return userService.addUser(user); } /** @@ -71,7 +60,7 @@ public User addUser(@Valid @RequestBody User user) { * * @return updated user. * @throws ValidationException if ID is null or not valid - * @throws NotFoundException if user is not found + * @throws NotFoundException if user is not found */ @PutMapping public User updateUser(@Valid @RequestBody User newUser) { @@ -79,16 +68,67 @@ public User updateUser(@Valid @RequestBody User newUser) { throw new ValidationException("Id должен быть указан"); } - if (users.containsKey(newUser.getId())) { - User oldUser = users.get(newUser.getId()); + return userService.updateUser(newUser); + } - users.put(newUser.getId(), newUser); + /** + * Handles GET method. + *

    Return collection of user`s friends. + * + * @param id user`s id. Must be positive number. + * @return collection of users. + * @throws NotFoundException if user is not found + */ + @GetMapping("/{id}/friends") + public Collection getFriends(@PathVariable @Positive Long id) { + return userService.getFriends(id); + } - log.info("PUT /users - User updated: {}", oldUser); + /** + * Handles GET method. + *

    Return collection of users that are common between two users. + * + * @param id user`s id. Must be positive number. + * @param otherId other user`s id. Must be positive number. + * @return collection of users. + * @throws NotFoundException if user is not found + */ + @GetMapping("/{id}/friends/common/{otherId}") + public Collection getCommonFriends(@PathVariable @Positive Long id, + @PathVariable @Positive Long otherId) { + return userService.getCommonFriends(id, otherId); + } - return newUser; - } + /** + * Handles PUT method. + *

    Creates friendship between two users. + * User`s consent is not required. + * + * @param id user`s id. Sender. Must be positive number. + * @param friendId friend`s id. Receiver. Must be positive number. + * @return collection of sender friends. + * @throws NotFoundException if user is not found + * @throws ValidationException if id equals friendId + */ + @PutMapping("/{id}/friends/{friendId}") + public Collection addFriend(@PathVariable @Positive Long id, + @PathVariable @Positive Long friendId) { + return userService.addFriend(id, friendId); + } - throw new NotFoundException("Пользователь с id = " + newUser.getId() + " не найден"); + /** + * Handles DELETE method. + *

    Breaks friendship between two users. + * User`s consent is not required. + * + * @param id user`s id. Sender. Must be positive number. + * @param friendId friend`s id. Receiver. Must be positive number. + * @throws NotFoundException if user is not found + */ + @DeleteMapping("/{id}/friends/{friendId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteFriend(@PathVariable @Positive Long id, + @PathVariable @Positive Long friendId) { + userService.deleteFriend(id, friendId); } } From 995c9d3b3f33f70bcc17ad04cadf1cdd8d6e0604 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 29 Oct 2025 23:32:54 +0300 Subject: [PATCH 20/35] feat: Completely reworked Controller. Deleted everything that controller should not have. All operations go through FilmService. Add new endpoints. --- .../filmorate/controller/FilmController.java | 73 ++++++++++++------- 1 file changed, 48 insertions(+), 25 deletions(-) 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 0936012..e9081fc 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -1,41 +1,39 @@ package ru.yandex.practicum.filmorate.controller; import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; 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.service.film.FilmService; import java.util.Collection; -import java.util.HashMap; -import java.util.concurrent.atomic.AtomicLong; @RestController @RequestMapping("/films") @Slf4j public class FilmController { - private final HashMap films = new HashMap<>(); - private final AtomicLong idGenerator = new AtomicLong(); + private final FilmService filmService; /** - * Private constructor to initialize ID generator with starting ID = 1. + * Constructor for dependency injection */ - private FilmController() { - idGenerator.set(1); + public FilmController(FilmService filmService) { + this.filmService = filmService; } /** * Handles GET method. - *

    Retrieves all films from the storage. + *

    Retrieves all filmsStorage from the storage. * - * @return Collection of all films. + * @return Collection of all filmsStorage. */ @GetMapping public Collection getFilms() { - log.info("GET /films - returning {} films", films.size()); - return films.values(); + return filmService.getAllFilms(); } /** @@ -46,13 +44,7 @@ public Collection getFilms() { */ @PostMapping public Film addFilm(@Valid @RequestBody Film film) { - - film.setId(idGenerator.getAndIncrement()); - films.put(film.getId(), film); - - log.info("POST /films - Film created: {}", film); - - return film; + return filmService.addFilm(film); } /** @@ -75,15 +67,46 @@ public Film updateFilm(@Valid @RequestBody Film newFilm) { throw new ValidationException("Id должен быть указан"); } - if (films.containsKey(newFilm.getId())) { - Film oldFilm = films.get(newFilm.getId()); + return filmService.updateFilm(newFilm); + } - films.put(newFilm.getId(), newFilm); - log.info("PUT /films - Film updated: {}", oldFilm); + /** + * Handles GET method. + *

    Retrieves top {@code count} popular films based on likes. + * + * @param count must be provided by url params, not required default value is 10. + * @return collection of films. + */ + @GetMapping("/popular") + public Collection getPopular(@RequestParam(required = false, defaultValue = "10") @Positive Long count) { + return filmService.getTopFilms(count); + } - return newFilm; - } + /** + * Handles PUT method. + *

    Adds like to specific film by specific user. If like from this user is already set - nothing happens. + * + * @param filmId Film`s id where likes amount should be increased. Must be positive number. + * @param userId User`s id who sets like to the film. Must be positive number. + * @return Film where like was set. + */ + @PutMapping("/{filmId}/like/{userId}") + public Film addLike(@PathVariable @Positive Long filmId, + @PathVariable @Positive Long userId) { + return filmService.addLike(filmId, userId); + } - throw new NotFoundException("Фильм с id = " + newFilm.getId() + " не найден"); + /** + * Handles PUT method. + *

    Deletes like from specific film by specific user. If like was never there - nothing happens. + * + * @param filmId Film`s id where likes amount should be decreased. Must be positive number. + * @param userId User`s id who deletes like to the film. Must be positive number. + * @return Film where like was deleted. + */ + @DeleteMapping("/{filmId}/like/{userId}") + public Film deleteLike(@PathVariable @Positive Long filmId, + @PathVariable @Positive Long userId) { + return filmService.removeLike(filmId, userId); } } From 9eb999c3d67abd5df710e019d7dea22a10b6cf1e Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 29 Oct 2025 23:33:46 +0300 Subject: [PATCH 21/35] test: Improved ControllerTestes, added more test for previous endpoints and wrote new test for new endpoints. --- .../filmorate/controller/ControllerTest.java | 5 - .../controller/FilmControllerTest.java | 448 ++++++++++++++++-- .../controller/UserControllerTest.java | 418 +++++++++++++--- 3 files changed, 741 insertions(+), 130 deletions(-) diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/ControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/ControllerTest.java index e7b1e9f..efcd4f2 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/ControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/ControllerTest.java @@ -2,10 +2,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.web.servlet.MockMvc; -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) public class ControllerTest { @Autowired @@ -13,7 +11,4 @@ public class ControllerTest { @Autowired protected ObjectMapper objectMapper; - - protected String json; - } diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java index 75ad2aa..ce4fdbf 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java @@ -1,15 +1,19 @@ package ru.yandex.practicum.filmorate.controller; -import com.fasterxml.jackson.core.JsonProcessingException; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; +import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.service.film.FilmService; import java.time.LocalDate; +import java.util.Collections; +import java.util.List; +import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -17,39 +21,125 @@ @WebMvcTest(FilmController.class) public class FilmControllerTest extends ControllerTest { + @MockBean + private FilmService filmService; - private void addTestFilm() throws Exception { - mockMvc.perform(post("/films") - .contentType(MediaType.APPLICATION_JSON) - .content(json)); - } + @Nested + class FilmControllerGetTest { - @BeforeEach - public void getJsonString() throws JsonProcessingException { + @Test + public void shouldGetFilms() throws Exception { + Film film1 = new Film(1L, "Film One", "Description One", + LocalDate.of(2020, 1, 1), 120); + Film film2 = new Film(2L, "Film Two", "Description Two", + LocalDate.of(2021, 1, 1), 130); - Film film = new Film(2L, - "TestFilm", - "TestDescription", - LocalDate.of(2000, 1, 1), - 120); + List films = List.of(film1, film2); - json = objectMapper.writeValueAsString(film); - } + // Настраиваем мок сервиса + when(filmService.getAllFilms()).thenReturn(films); - @Nested - class FilmControllerGetTest { + mockMvc.perform(get("/films")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$.length()").value(2)) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[0].name").value("Film One")) + .andExpect(jsonPath("$[1].id").value(2)) + .andExpect(jsonPath("$[1].name").value("Film Two")); + + verify(filmService, times(1)).getAllFilms(); + } @Test - public void shouldGetFilms() throws Exception { - addTestFilm(); - addTestFilm(); + public void shouldGetEmptyList() throws Exception { + + when(filmService.getAllFilms()).thenReturn(Collections.emptyList()); mockMvc.perform(get("/films")) .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].id").value("1")) - .andExpect(jsonPath("$[0].name").value("TestFilm")) - .andExpect(jsonPath("$[1].id").value("2")) - .andExpect(jsonPath("$[1].name").value("TestFilm")); + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$.length()").value(0)); + + verify(filmService, times(1)).getAllFilms(); + } + + @Test + public void shouldGetPopularFilms() throws Exception { + Film film1 = new Film(1L, "Film One", "Description One", + LocalDate.of(2020, 1, 1), 120); + Film film2 = new Film(2L, "Film Two", "Description Two", + LocalDate.of(2021, 1, 1), 130); + film1.setLikes(2L); + film2.setLikes(1L); + List films = List.of(film1, film2); + + // Настраиваем мок сервиса + when(filmService.getTopFilms(10L)).thenReturn(films); + + mockMvc.perform(get("/films/popular")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$.length()").value(2)) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[0].likes").value(2)) + .andExpect(jsonPath("$[1].id").value(2)) + .andExpect(jsonPath("$[1].likes").value(1)); + + verify(filmService, times(1)).getTopFilms(10L); + } + + @Test + public void shouldGetOnlyCountPopularFilms() throws Exception { + Film film1 = new Film(1L, "Film One", "Description One", + LocalDate.of(2020, 1, 1), 120); + Film film2 = new Film(2L, "Film Two", "Description Two", + LocalDate.of(2021, 1, 1), 130); + film1.setLikes(2L); + film2.setLikes(1L); + + List films = List.of(film1, film2); + + // Настраиваем мок сервиса + when(filmService.getTopFilms(2L)).thenReturn(films); + + mockMvc.perform(get("/films/popular").param("count", "2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$.length()").value(2)) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[0].likes").value(2)) + .andExpect(jsonPath("$[1].id").value(2)) + .andExpect(jsonPath("$[1].likes").value(1)); + + verify(filmService, times(1)).getTopFilms(2L); + } + + @Test + public void shouldReturn400WhenInvalidCountParam() throws Exception { + mockMvc.perform(get("/films/popular") + .param("count", "not_a_number")) + .andExpect(status().isBadRequest()); + + verify(filmService, never()).getTopFilms(any()); + } + + @Test + public void shouldReturn400WhenNegativeCount() throws Exception { + mockMvc.perform(get("/films/popular") + .param("count", "-5")) + .andExpect(status().isBadRequest()); + + verify(filmService, never()).getTopFilms(any()); + } + + @Test + public void shouldReturn400WhenZeroCount() throws Exception { + mockMvc.perform(get("/films/popular") + .param("count", "0")) + .andExpect(status().isBadRequest()); + + verify(filmService, never()).getTopFilms(any()); } } @@ -58,20 +148,96 @@ class FilmControllerPostTest { @Test public void shouldCreateFilm() throws Exception { + Film filmToCreate = new Film(null, "TestFilm", "TestDescription", + LocalDate.of(2000, 1, 1), 120); + + Film createdFilm = new Film(1L, "TestFilm", "TestDescription", + LocalDate.of(2000, 1, 1), 120); + + when(filmService.addFilm(any(Film.class))).thenReturn(createdFilm); + + String filmJson = objectMapper.writeValueAsString(filmToCreate); mockMvc.perform(post("/films") .contentType(MediaType.APPLICATION_JSON) - .content(json)) + .content(filmJson)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.id").value("1")) + .andExpect(jsonPath("$.id").value(1)) .andExpect(jsonPath("$.name").value("TestFilm")) .andExpect(jsonPath("$.description").value("TestDescription")) .andExpect(jsonPath("$.releaseDate").value("2000-01-01")) - .andExpect(jsonPath("$.duration").value("120")); + .andExpect(jsonPath("$.duration").value(120)) + .andExpect(jsonPath("$.likes").value(0)); + + verify(filmService).addFilm(any(Film.class)); } @Test - public void shouldNotCreateFilmWhenInvalidJson() throws Exception { + public void shouldNotCreateFilmWhenNameIsBlankAndReturnBadRequest() throws Exception { + Film filmToCreate = new Film(null, "", "TestDescription", + LocalDate.of(2000, 1, 1), 120); + + String invalidJson = objectMapper.writeValueAsString(filmToCreate); + + + mockMvc.perform(post("/films") + .contentType(MediaType.APPLICATION_JSON) + .content(invalidJson)) + .andExpect(status().isBadRequest()); + + verify(filmService, never()).addFilm(any()); + } + + @Test + public void shouldNotCreateFilmWhenDescriptionIsOverMaxSizeAndReturnBadRequest() throws Exception { + Film filmToCreate = new Film(null, "name", "a".repeat(300), + LocalDate.of(2000, 1, 1), 120); + + String invalidJson = objectMapper.writeValueAsString(filmToCreate); + + + mockMvc.perform(post("/films") + .contentType(MediaType.APPLICATION_JSON) + .content(invalidJson)) + .andExpect(status().isBadRequest()); + + verify(filmService, never()).addFilm(any()); + } + + @Test + public void shouldNotCreateFilmWhenReleaseDateIsInvalidAndReturnBadRequest() throws Exception { + Film filmToCreate = new Film(null, "name", "description", + LocalDate.of(1000, 1, 1), 120); + + String invalidJson = objectMapper.writeValueAsString(filmToCreate); + + + mockMvc.perform(post("/films") + .contentType(MediaType.APPLICATION_JSON) + .content(invalidJson)) + .andExpect(status().isBadRequest()); + + verify(filmService, never()).addFilm(any()); + } + + @Test + public void shouldNotCreateFilmWhenDurationIsInvalidAndReturnBadRequest() throws Exception { + Film filmToCreate = new Film(null, "name", "description", + LocalDate.of(2000, 1, 1), -10); + + String invalidJson = objectMapper.writeValueAsString(filmToCreate); + + + mockMvc.perform(post("/films") + .contentType(MediaType.APPLICATION_JSON) + .content(invalidJson)) + .andExpect(status().isBadRequest()); + + verify(filmService, never()).addFilm(any()); + } + + @Test + public void shouldNotCreateFilmWhenInvalidJsonAndReturnBadRequest() throws Exception { String json = "{}"; mockMvc.perform(post("/films") @@ -79,65 +245,249 @@ public void shouldNotCreateFilmWhenInvalidJson() throws Exception { .content(json)) .andExpect(status().isBadRequest()); } - } - @Nested - class FilmControllerPutTest { + @Test + public void shouldReturn400WhenInvalidJsonSyntax() throws Exception { + String invalidJson = "{ invalid json syntax ;"; + + mockMvc.perform(post("/films") + .contentType(MediaType.APPLICATION_JSON) + .content(invalidJson)) + .andExpect(status().isBadRequest()); + + verify(filmService, never()).addFilm(any()); + } - @BeforeEach - public void getNewText() throws JsonProcessingException { - Film film = new Film(1L, - "ONLY TODAY", - "NEW TEXT", - LocalDate.of(2000, 1, 1), - 120); + @Test + public void shouldReturn400WhenInvalidDurationType() throws Exception { + String invalidJson = + "{\"name\":\"FilmName\",\"description\":\"Description\",\"releaseDate\":\"2000-01-01\",\"duration\":\"not_a_number\"}"; - json = objectMapper.writeValueAsString(film); + mockMvc.perform(post("/films") + .contentType(MediaType.APPLICATION_JSON) + .content(invalidJson)) + .andExpect(status().isBadRequest()); + + verify(filmService, never()).addFilm(any()); } + @Test + public void shouldReturn400WhenInvalidDateFormat() throws Exception { + String invalidJson = + "{\"name\":\"FilmName\",\"description\":\"Description\",\"releaseDate\":\"2000/01/01\",\"duration\":120}"; + + mockMvc.perform(post("/films") + .contentType(MediaType.APPLICATION_JSON) + .content(invalidJson)) + .andExpect(status().isBadRequest()); + + verify(filmService, never()).addFilm(any()); + } + } + + @Nested + class FilmControllerPutTest { + @Test public void shouldUpdateFilm() throws Exception { - addTestFilm(); + Film filmToUpdate = new Film(1L, "ONLY TODAY", "NEW TEXT", + LocalDate.of(2000, 1, 1), 120); + filmToUpdate.setLikes(5L); + + Film updatedFilm = new Film(1L, "ONLY TODAY", "NEW TEXT", + LocalDate.of(2000, 1, 1), 120); + updatedFilm.setLikes(5L); + + when(filmService.updateFilm(any(Film.class))).thenReturn(updatedFilm); + + String json = objectMapper.writeValueAsString(filmToUpdate); mockMvc.perform(put("/films") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) .andExpect(jsonPath("$.name").value("ONLY TODAY")) .andExpect(jsonPath("$.description").value("NEW TEXT")) .andExpect(jsonPath("$.releaseDate").value("2000-01-01")) - .andExpect(jsonPath("$.duration").value("120")); + .andExpect(jsonPath("$.duration").value(120)) + .andExpect(jsonPath("$.likes").value(5)); + + verify(filmService, times(1)).updateFilm(any(Film.class)); } @Test - public void shouldNotUpdateFilmWhenNoId() throws Exception { - String json = - "{\"name\":\"test\", \"description\":\"test\", \"releaseDate\":\"2000-01-01\", \"name\":120}"; - addTestFilm(); + public void shouldNotUpdateFilmWhenNoIdAndReturnBadRequest() throws Exception { + Film filmToUpdate = new Film(null, "ONLY TODAY", "NEW TEXT", + LocalDate.of(2000, 1, 1), 120); + + String json = objectMapper.writeValueAsString(filmToUpdate); mockMvc.perform(put("/films") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isBadRequest()); + + verify(filmService, never()).updateFilm(any()); } @Test public void shouldNotUpdateFilmWhenNotFound() throws Exception { + Film filmToUpdate = new Film(1000L, "ONLY TODAY", "NEW TEXT", + LocalDate.of(2000, 1, 1), 120); + + when(filmService.updateFilm(any(Film.class))) + .thenThrow(new NotFoundException("Фильм с id = 1000 не найден")); + + String json = objectMapper.writeValueAsString(filmToUpdate); + mockMvc.perform(put("/films") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isNotFound()); + + verify(filmService, times(1)).updateFilm(any(Film.class)); } @Test - public void shouldNotUpdateFilmWhenInvalidJson() throws Exception { - String json = "{}"; + public void shouldAddLike() throws Exception { + Film film = new Film(1L, "ONLY TODAY", "NEW TEXT", + LocalDate.of(2000, 1, 1), 120); + film.setLikes(5L); - mockMvc.perform(put("/films") + Film updatedFilm = new Film(1L, "ONLY TODAY", "NEW TEXT", + LocalDate.of(2000, 1, 1), 120); + updatedFilm.setLikes(6L); + + when(filmService.addLike(any(), any())).thenReturn(updatedFilm); + + String json = objectMapper.writeValueAsString(film); + + mockMvc.perform(put("/films/1/like/1") .contentType(MediaType.APPLICATION_JSON) .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("ONLY TODAY")) + .andExpect(jsonPath("$.description").value("NEW TEXT")) + .andExpect(jsonPath("$.releaseDate").value("2000-01-01")) + .andExpect(jsonPath("$.duration").value(120)) + .andExpect(jsonPath("$.likes").value(6)); + + verify(filmService, times(1)).addLike(any(), any()); + } + + @Test + public void shouldNotAddLikeWhenFilmNotFound() throws Exception { + Film filmToUpdate = new Film(1000L, "ONLY TODAY", "NEW TEXT", + LocalDate.of(2000, 1, 1), 120); + + when(filmService.addLike(any(), any())) + .thenThrow(new NotFoundException("Фильм с id = 1000 не найден")); + + String json = objectMapper.writeValueAsString(filmToUpdate); + + mockMvc.perform(put("/films/1000/like/1") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isNotFound()); + + verify(filmService, times(1)).addLike(any(), any()); + } + + @Test + public void shouldNotAddLikeWhenUserNotFound() throws Exception { + Film filmToUpdate = new Film(1L, "ONLY TODAY", "NEW TEXT", + LocalDate.of(2000, 1, 1), 120); + + when(filmService.addLike(any(), any())) + .thenThrow(new NotFoundException("Пользователь с id = 1000 не найден")); + + String json = objectMapper.writeValueAsString(filmToUpdate); + + mockMvc.perform(put("/films/1/like/1000") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isNotFound()); + + verify(filmService, times(1)).addLike(any(), any()); + } + + @Test + public void shouldReturn400WhenInvalidIdsInLike() throws Exception { + mockMvc.perform(put("/films/invalid_id/like/also_invalid")) .andExpect(status().isBadRequest()); + + verify(filmService, never()).addLike(any(), any()); + } + } + + @Nested + class FilmControllerDeleteTest { + + @Test + public void shouldRemoveLike() throws Exception { + Film film = new Film(1L, "ONLY TODAY", "NEW TEXT", + LocalDate.of(2000, 1, 1), 120); + film.setLikes(5L); + + Film updatedFilm = new Film(1L, "ONLY TODAY", "NEW TEXT", + LocalDate.of(2000, 1, 1), 120); + updatedFilm.setLikes(4L); + + when(filmService.removeLike(any(), any())).thenReturn(updatedFilm); + + String json = objectMapper.writeValueAsString(film); + + mockMvc.perform(delete("/films/1/like/1") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("ONLY TODAY")) + .andExpect(jsonPath("$.description").value("NEW TEXT")) + .andExpect(jsonPath("$.releaseDate").value("2000-01-01")) + .andExpect(jsonPath("$.duration").value(120)) + .andExpect(jsonPath("$.likes").value(4)); + + verify(filmService, times(1)).removeLike(any(), any()); + } + + @Test + public void shouldNotRemoveLikeWhenFilmNotFound() throws Exception { + Film filmToUpdate = new Film(1000L, "ONLY TODAY", "NEW TEXT", + LocalDate.of(2000, 1, 1), 120); + + when(filmService.removeLike(any(), any())) + .thenThrow(new NotFoundException("Фильм с id = 1000 не найден")); + + String json = objectMapper.writeValueAsString(filmToUpdate); + + mockMvc.perform(delete("/films/1000/like/1") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isNotFound()); + + verify(filmService, times(1)).removeLike(any(), any()); + } + + @Test + public void shouldNotRemoveLikeWhenUserNotFound() throws Exception { + Film filmToUpdate = new Film(1L, "ONLY TODAY", "NEW TEXT", + LocalDate.of(2000, 1, 1), 120); + + when(filmService.removeLike(any(), any())) + .thenThrow(new NotFoundException("Пользователь с id = 1000 не найден")); + + String json = objectMapper.writeValueAsString(filmToUpdate); + + mockMvc.perform(delete("/films/1/like/1000") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isNotFound()); + + verify(filmService, times(1)).removeLike(any(), any()); } } } \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java index d9b5cb5..9658123 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java @@ -1,15 +1,19 @@ package ru.yandex.practicum.filmorate.controller; -import com.fasterxml.jackson.core.JsonProcessingException; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; +import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.service.user.UserService; import java.time.LocalDate; +import java.util.Collections; +import java.util.List; +import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -17,153 +21,415 @@ @WebMvcTest(UserController.class) public class UserControllerTest extends ControllerTest { - private void addTestUser() throws Exception { - mockMvc.perform(post("/users") - .contentType(MediaType.APPLICATION_JSON) - .content(json)); - } + @MockBean + private UserService userService; - @BeforeEach - public void getJsonString() throws JsonProcessingException { + @Nested + class UserControllerGetTest { - User user = new User(2L, - "valid@mail.com", - "login", - "George", - LocalDate.of(2000, 1, 1) - ); + @Test + public void shouldGetUsers() throws Exception { + User user1 = new User(1L, "email1", "login1", "name1", LocalDate.now()); + User user2 = new User(2L, "email2", "login2", "name2", LocalDate.now()); - json = objectMapper.writeValueAsString(user); + List users = List.of(user1, user2); - } + // Настраиваем мок сервиса + when(userService.getAllUsers()).thenReturn(users); - @Nested - class UserGetTest { + mockMvc.perform(get("/users")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$.length()").value(2)) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[0].login").value("login1")) + .andExpect(jsonPath("$[1].id").value(2)) + .andExpect(jsonPath("$[1].login").value("login2")); + + verify(userService, times(1)).getAllUsers(); + } @Test - public void shouldGetUsers() throws Exception { - addTestUser(); - addTestUser(); + public void shouldGetEmptyList() throws Exception { + + when(userService.getAllUsers()).thenReturn(Collections.emptyList()); mockMvc.perform(get("/users")) .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].id").value("1")) - .andExpect(jsonPath("$[0].email").value("valid@mail.com")) - .andExpect(jsonPath("$[1].id").value("2")) - .andExpect(jsonPath("$[1].email").value("valid@mail.com")); + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$.length()").value(0)); + + verify(userService, times(1)).getAllUsers(); + } + + @Test + public void shouldGetFriends() throws Exception { + User user2 = new User(2L, "email2", "login2", "name2", LocalDate.now()); + User user3 = new User(3L, "email3", "login3", "name3", LocalDate.now()); + + List users = List.of(user2, user3); + + // Настраиваем мок сервиса + when(userService.getFriends(1L)).thenReturn(users); + + mockMvc.perform(get("/users/1/friends")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$.length()").value(2)) + .andExpect(jsonPath("$[0].id").value(2)) + .andExpect(jsonPath("$[0].login").value("login2")) + .andExpect(jsonPath("$[1].id").value(3)) + .andExpect(jsonPath("$[1].login").value("login3")); + + verify(userService, times(1)).getFriends(1L); + } + + @Test + public void shouldReturnNotFoundWhenUserIsNotFound() throws Exception { + + // Настраиваем мок сервиса + when(userService.getFriends(1000L)) + .thenThrow(new NotFoundException("Пользователь с id = 1000 не найден")); + + + mockMvc.perform(get("/users/1000/friends")) + .andExpect(status().isNotFound()); + + verify(userService, times(1)).getFriends(1000L); + } + + @Test + public void shouldGetCommonFriends() throws Exception { + User user2 = new User(2L, "email2", "login2", "name2", LocalDate.now()); + User user3 = new User(3L, "email3", "login3", "name3", LocalDate.now()); + + List users = List.of(user2, user3); + + // Настраиваем мок сервиса + when(userService.getCommonFriends(1L, 4L)).thenReturn(users); + + mockMvc.perform(get("/users/1/friends/common/4")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$.length()").value(2)) + .andExpect(jsonPath("$[0].id").value(2)) + .andExpect(jsonPath("$[0].login").value("login2")) + .andExpect(jsonPath("$[1].id").value(3)) + .andExpect(jsonPath("$[1].login").value("login3")); + + verify(userService, times(1)).getCommonFriends(1L, 4L); + } + + @Test + public void shouldReturnNotFoundWhenUserIsNotFoundWhenCommonFriends() throws Exception { + + // Настраиваем мок сервиса + when(userService.getCommonFriends(1000L, 1L)) + .thenThrow(new NotFoundException("Пользователь с id = 1000 не найден")); + + + mockMvc.perform(get("/users/1000/friends/common/1")) + .andExpect(status().isNotFound()); + + verify(userService, times(1)).getCommonFriends(1000L, 1L); + } + + @Test + public void shouldReturnNotFoundWhenOtherUserIsNotFoundWhenCommonFriends() throws Exception { + + // Настраиваем мок сервиса + when(userService.getCommonFriends(1L, 1000L)) + .thenThrow(new NotFoundException("Пользователь с id = 1000 не найден")); + + + mockMvc.perform(get("/users/1/friends/common/1000")) + .andExpect(status().isNotFound()); + + verify(userService, times(1)).getCommonFriends(1L, 1000L); + } + + @Test + public void shouldReturn400WhenInvalidIdsInFriends() throws Exception { + mockMvc.perform(get("/users/invalid_id/friends/common/also_invalid")) + .andExpect(status().isBadRequest()); + + verify(userService, never()).getCommonFriends(any(), any()); + } + + @Test + public void shouldReturn400WhenNegativeIds() throws Exception { + mockMvc.perform(get("/users/-1/friends/common/-2")) + .andExpect(status().isBadRequest()); + + verify(userService, never()).getFriends(any()); + } + + @Test + public void shouldReturn400WhenZeroUserId() throws Exception { + mockMvc.perform(get("/users/0/friends")) + .andExpect(status().isBadRequest()); + + verify(userService, never()).getFriends(any()); } } @Nested - class UserPostTest { + class UserControllerPostTest { @Test public void shouldCreateUser() throws Exception { + User user = new User(null, "email@mail.org", "login1", "name1", LocalDate.now()); + User createdUser = new User(1L, "email@mail.org", "login1", "name1", LocalDate.now()); + + when(userService.addUser(any(User.class))).thenReturn(createdUser); + + String json = objectMapper.writeValueAsString(user); mockMvc.perform(post("/users") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.id").value("1")) - .andExpect(jsonPath("$.email").value("valid@mail.com")) - .andExpect(jsonPath("$.login").value("login")) - .andExpect(jsonPath("$.name").value("George")) - .andExpect(jsonPath("$.birthday").value("2000-01-01")); + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.email").value("email@mail.org")) + .andExpect(jsonPath("$.login").value("login1")) + .andExpect(jsonPath("$.name").value("name1")); + + verify(userService).addUser(any(User.class)); } @Test - public void shouldCreateUserAndNameShouldBeLoginIfNameNull() throws Exception { + public void shouldReturnBadRequestWhenEmailIsNotValid() throws Exception { + User user = new User(null, "@email", "login1", "name1", LocalDate.now()); - User user = new User(2L, - "valid@mail.com", - "login", - null, - LocalDate.of(2000, 1, 1) - ); + String json = objectMapper.writeValueAsString(user); - json = objectMapper.writeValueAsString(user); + mockMvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + + verify(userService, never()).addUser(any(User.class)); + } + + @Test + public void shouldReturnBadRequestWhenEmailIsBlank() throws Exception { + User user = new User(null, " ", "login1", "name1", LocalDate.now()); + + String json = objectMapper.writeValueAsString(user); mockMvc.perform(post("/users") .contentType(MediaType.APPLICATION_JSON) .content(json)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.id").value("1")) - .andExpect(jsonPath("$.email").value("valid@mail.com")) - .andExpect(jsonPath("$.login").value("login")) - .andExpect(jsonPath("$.name").value("login")) - .andExpect(jsonPath("$.birthday").value("2000-01-01")); + .andExpect(status().isBadRequest()); + + verify(userService, never()).addUser(any(User.class)); } @Test - public void shouldNotCreateUserWhenInvalidJson() throws Exception { - String json = "{}"; + public void shouldReturnBadRequestWhenLoginContainsWhiteSpaces() throws Exception { + User user = new User(null, "email@mail.org", " logi n 1", "name1", LocalDate.now()); + + String json = objectMapper.writeValueAsString(user); mockMvc.perform(post("/users") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isBadRequest()); + + verify(userService, never()).addUser(any(User.class)); } - } - @Nested - class UserPutTest { + @Test + public void shouldReturnBadRequestWhenLoginBirthDayInFuture() throws Exception { + User user = new User(null, "email@mail.org", "login1", "name1", LocalDate.now().plusDays(1)); + + String json = objectMapper.writeValueAsString(user); - @BeforeEach - public void getNewText() throws JsonProcessingException { - User user = new User(2L, - "valid@mail.com", - "newlogin", - "same George", - LocalDate.of(2000, 1, 1) - ); + mockMvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + + verify(userService, never()).addUser(any(User.class)); + } + + @Test + public void shouldReturn400WhenInvalidJsonSyntax() throws Exception { + String invalidJson = "{ name: user, email: test@mail.ru }"; + + mockMvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(invalidJson)) + .andExpect(status().isBadRequest()); - json = objectMapper.writeValueAsString(user); + verify(userService, never()).addUser(any()); } + @Test + public void shouldReturn400WhenInvalidBirthdayFormat() throws Exception { + String invalidJson = "{\"email\":\"test@mail.ru\",\"login\":\"login\",\"name\":\"Name\",\"birthday\":\"2000/01/01\"}"; + + mockMvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(invalidJson)) + .andExpect(status().isBadRequest()); + + verify(userService, never()).addUser(any()); + } + + @Test + public void shouldReturn400WhenFutureBirthday() throws Exception { + String invalidJson = "{\"email\":\"test@mail.ru\",\"login\":\"login\",\"name\":\"Name\",\"birthday\":\"2030-01-01\"}"; + + mockMvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(invalidJson)) + .andExpect(status().isBadRequest()); + + verify(userService, never()).addUser(any()); + } + } + + @Nested + class UserControllerPutTest { + @Test public void shouldUpdateUser() throws Exception { - addTestUser(); - addTestUser(); + User userToUpdate = new User(1L, "email@mail.org", "login1", "name1", LocalDate.now()); + User updatedUser = new User(1L, "email@mail.org", "login1", "name1", LocalDate.now()); + + when(userService.updateUser(any(User.class))).thenReturn(updatedUser); + + String json = objectMapper.writeValueAsString(userToUpdate); mockMvc.perform(put("/users") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.email").value("valid@mail.com")) - .andExpect(jsonPath("$.login").value("newlogin")) - .andExpect(jsonPath("$.name").value("same George")) - .andExpect(jsonPath("$.birthday").value("2000-01-01")); + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.email").value("email@mail.org")) + .andExpect(jsonPath("$.login").value("login1")) + .andExpect(jsonPath("$.name").value("name1")); + + verify(userService, times(1)).updateUser(any(User.class)); } @Test - public void shouldNotUpdateUserWhenNoId() throws Exception { - String json = - "{\"email\":\"valid@mail.com\", \"login\":\"test\", \"name\":\"test\", \"birthday\":2000-01-01}"; - addTestUser(); + public void shouldNotUpdateFilmWhenNoIdAndReturnBadRequest() throws Exception { + User userToUpdate = new User(null, "email@mail.org", "login1", "name1", LocalDate.now()); + + String json = objectMapper.writeValueAsString(userToUpdate); mockMvc.perform(put("/users") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isBadRequest()); + + verify(userService, never()).updateUser(any()); } @Test - public void shouldNotUpdateUserWhenNotFound() throws Exception { + public void shouldNotUpdateFilmWhenNotFound() throws Exception { + User userToUpdate = new User(1000L, "email@mail.org", "login1", "name1", LocalDate.now()); + + when(userService.updateUser(any(User.class))) + .thenThrow(new NotFoundException("Пользователь с id = 1000 не найден")); + + String json = objectMapper.writeValueAsString(userToUpdate); + mockMvc.perform(put("/users") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isNotFound()); + + verify(userService, times(1)).updateUser(any(User.class)); } @Test - public void shouldNotUpdateUserWhenInvalidJson() throws Exception { - String json = "{}"; + public void shouldAddFriend() throws Exception { + User user1 = new User(1L, "email1", "login1", "name1", LocalDate.now()); + User user2 = new User(2L, "email2", "login2", "name2", LocalDate.now()); - mockMvc.perform(put("/users") - .contentType(MediaType.APPLICATION_JSON) - .content(json)) + List list = List.of(user1, user2); + + when(userService.addFriend(any(), any())).thenReturn(list); + + mockMvc.perform(put("/users/3/friends/2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$.length()").value(2)); + + verify(userService, times(1)).addFriend(any(), any()); + } + + @Test + public void shouldReturnNotFoundWhenSenderNotFound() throws Exception { + + when(userService.addFriend(1000L, 1L)) + .thenThrow(new NotFoundException("Пользователь с id = 1000 не найден")); + + + mockMvc.perform(put("/users/1000/friends/1")) + .andExpect(status().isNotFound()); + + verify(userService, times(1)).addFriend(1000L, 1L); + } + + @Test + public void shouldReturnNotFoundWhenReceiverNotFound() throws Exception { + + when(userService.addFriend(1L, 1000L)) + .thenThrow(new NotFoundException("Пользователь с id = 1000 не найден")); + + + mockMvc.perform(put("/users/1/friends/1000")) + .andExpect(status().isNotFound()); + + verify(userService, times(1)).addFriend(1L, 1000L); + } + + @Test + public void shouldReturn400WhenInvalidFriendId() throws Exception { + mockMvc.perform(put("/users/1/friends/not_a_number")) .andExpect(status().isBadRequest()); + + verify(userService, never()).addFriend(any(), any()); + } + } + + @Nested + class UserControllerDeleteTest { + + @Test + public void shouldRemoveFriend() throws Exception { + mockMvc.perform(delete("/users/1/friends/2")) + .andExpect(status().isNoContent()); + + verify(userService, times(1)).deleteFriend(any(), any()); + } + + @Test + public void shouldReturnNotFoundWhenUserNotFound() throws Exception { + doThrow(new NotFoundException("Пользователь с id = 1000 не найден")) + .when(userService).deleteFriend(1000L, 1L); + + mockMvc.perform(delete("/users/1000/friends/1")) + .andExpect(status().isNotFound()); + + verify(userService, times(1)).deleteFriend(any(), any()); + } + + @Test + public void shouldReturnNotFoundWhenOtherUserNotFound() throws Exception { + doThrow(new NotFoundException("Пользователь с id = 1000 не найден")) + .when(userService).deleteFriend(1L, 1000L); + + mockMvc.perform(delete("/users/1/friends/1000")) + .andExpect(status().isNotFound()); + + verify(userService, times(1)).deleteFriend(any(), any()); } } } \ No newline at end of file From ca135bf2ffdf02230a35369e0f3d506a78b4a7e6 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 29 Oct 2025 23:34:19 +0300 Subject: [PATCH 22/35] test: Wrote tests for methods that have their own logic (not just call for storage) --- .../service/film/FilmLikeServiceTest.java | 120 ++++++++++++++++++ .../service/user/FriendShipServiceTest.java | 64 ++++++++++ .../service/user/UserServiceTest.java | 99 +++++++++++++++ 3 files changed, 283 insertions(+) create mode 100644 src/test/java/ru/yandex/practicum/filmorate/service/film/FilmLikeServiceTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/service/user/FriendShipServiceTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/service/user/UserServiceTest.java diff --git a/src/test/java/ru/yandex/practicum/filmorate/service/film/FilmLikeServiceTest.java b/src/test/java/ru/yandex/practicum/filmorate/service/film/FilmLikeServiceTest.java new file mode 100644 index 0000000..7ce4e83 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/service/film/FilmLikeServiceTest.java @@ -0,0 +1,120 @@ +package ru.yandex.practicum.filmorate.service.film; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.storage.film.FilmStorage; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class FilmLikeServiceTest { + + @Mock + private FilmStorage filmStorage; + + @Mock + private UserStorage userStorage; + + private FilmLikeService filmLikeService; + + @BeforeEach + void setUp() { + filmLikeService = new FilmLikeService(filmStorage, userStorage); + } + + @Test + void shouldAddLikeWhenUserExist() { + // входящая информация + Long filmId = 1L; + Long userId = 1L; + Film film = new Film(); + film.setId(filmId); + + // что мы должны получить + Film filmWithLike = new Film(); + filmWithLike.setId(filmId); + filmWithLike.setLikes(1L); + + // настраиваем поведение мока + doNothing().when(userStorage).throwIfNotFound(userId); + when(filmStorage.addLike(filmId, userId)).thenReturn(filmWithLike); + + Film result = filmLikeService.addLike(filmId, userId); + + // проверки, что метод вызвался + verify(userStorage).throwIfNotFound(userId); + verify(filmStorage).addLike(filmId, userId); + + // проверка, что значения равны + assertEquals(1, result.getLikes()); + } + + @Test + void shouldNotAddLikeAndThrowNotFoundExceptionWhenUserDoesNotExist() { + Long userId = 1000L; + Long filmId = 1L; + Film film = new Film(); + film.setId(filmId); + + // говорю моку, что ему нужно НЕ НАЙТИ юзера + doThrow(NotFoundException.class).when(userStorage).throwIfNotFound(userId); + + assertThrows(NotFoundException.class, () -> filmLikeService.addLike(filmId, userId)); + + verify(userStorage).throwIfNotFound(userId); + verify(filmStorage, never()).addLike(filmId, userId); + } + + @Test + void shouldRemoveLikeWhenUserExist() { + // входящая информация + Long filmId = 1L; + Long userId = 1L; + Film film = new Film(); + film.setId(filmId); + film.setLikes(1L); + + // что мы должны получить + Film filmWithLike = new Film(); + filmWithLike.setId(filmId); + filmWithLike.setLikes(0L); + + // настраиваем поведение мока + doNothing().when(userStorage).throwIfNotFound(userId); + when(filmStorage.removeLike(filmId, userId)).thenReturn(filmWithLike); + + Film result = filmLikeService.removeLike(filmId, userId); + + // проверки, что метод вызвался + verify(userStorage).throwIfNotFound(userId); + verify(filmStorage).removeLike(filmId, userId); + + // проверка, что значения равны + assertEquals(0, result.getLikes()); + } + + @Test + void shouldNotRemoveLikeAndThrowNotFoundExceptionWhenUserDoesNotExist() { + Long userId = 1000L; + Long filmId = 1L; + Film film = new Film(); + film.setId(filmId); + film.setLikes(1L); + + // говорю моку, что ему нужно НЕ НАЙТИ юзера + doThrow(NotFoundException.class).when(userStorage).throwIfNotFound(userId); + + assertThrows(NotFoundException.class, () -> filmLikeService.removeLike(filmId, userId)); + + verify(userStorage).throwIfNotFound(userId); + verify(filmStorage, never()).addLike(filmId, userId); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/service/user/FriendShipServiceTest.java b/src/test/java/ru/yandex/practicum/filmorate/service/user/FriendShipServiceTest.java new file mode 100644 index 0000000..a59f900 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/service/user/FriendShipServiceTest.java @@ -0,0 +1,64 @@ +package ru.yandex.practicum.filmorate.service.user; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import ru.yandex.practicum.filmorate.exception.ValidationException; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; + +import java.time.LocalDate; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class FriendShipServiceTest { + + @Mock + private UserStorage userStorage; + + private FriendShipService friendShipService; + + @BeforeEach + void setUp() { + friendShipService = new FriendShipService(userStorage); + } + + public Collection addFriend(Long senderId, Long receiverId) { + if (Objects.equals(receiverId, senderId)) { + throw new ValidationException("Сам себя не добавишь - никто не добавит"); + } + + return userStorage.addFriend(senderId, receiverId); + } + + @Test + void shouldThrowValidationExceptionWhenIdsAreEqual() { + assertThrows(ValidationException.class, () -> friendShipService.addFriend(1L, 1L)); + verify(userStorage, never()).addFriend(any(), any()); + } + + @Test + void shouldReturnCollectionWhenIdsAreNotEqual() { + Long senderId = 1L; + Long receiverId = 2L; + List expectedFriends = List.of( + new User(2L, "email", "friend", "Friend", LocalDate.now()) + ); + + when(userStorage.addFriend(senderId, receiverId)).thenReturn(expectedFriends); + + Collection result = friendShipService.addFriend(senderId, receiverId); + + assertThat(result, is(expectedFriends)); + verify(userStorage).addFriend(senderId, receiverId); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/service/user/UserServiceTest.java b/src/test/java/ru/yandex/practicum/filmorate/service/user/UserServiceTest.java new file mode 100644 index 0000000..c8c7cd6 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/service/user/UserServiceTest.java @@ -0,0 +1,99 @@ +package ru.yandex.practicum.filmorate.service.user; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class UserServiceTest { + + @Mock + private UserStorage userStorage; + + @Mock + private FriendShipService friendShipService; + + private UserService userService; + + @BeforeEach + void setUp() { + userService = new UserService(userStorage, friendShipService); + } + + @Test + void shouldSetNameAsLoginWhenNameIsNull() { + // входящая информация + User user = new User(1L, "", "login", null, LocalDate.now()); + + // настраиваем поведение мока + when(userStorage.add(user)).thenReturn(user); + + User result = userService.addUser(user); + + // проверки, что метод вызвался + verify(userStorage).add(user); + + // проверка, что значения равны + assertEquals("login", result.getName()); + } + + @Test + void shouldSetNameAsLoginWhenNameIsEmpty() { + // входящая информация + User user = new User(1L, "", "login", "", LocalDate.now()); + + // настраиваем поведение мока + when(userStorage.add(user)).thenReturn(user); + + User result = userService.addUser(user); + + // проверки, что метод вызвался + verify(userStorage).add(user); + + // проверка, что значения равны + assertEquals("login", result.getName()); + } + + @Test + void shouldSetNameAsLoginWhenNameIsBlank() { + // входящая информация + User user = new User(1L, "", "login", " ", LocalDate.now()); + + // настраиваем поведение мока + when(userStorage.add(user)).thenReturn(user); + + User result = userService.addUser(user); + + // проверки, что метод вызвался + verify(userStorage).add(user); + // проверка, что значения равны + assertEquals("login", result.getName()); + } + + @Test + void shouldDoNothingWhenNameIsPresent() { + // входящая информация + User user = new User(1L, "", "login", "name", LocalDate.now()); + + // настраиваем поведение мока + when(userStorage.add(user)).thenReturn(user); + + User result = userService.addUser(user); + + // проверки, что метод вызвался + verify(userStorage).add(user); + + // проверка, что значения равны + assertEquals("name", result.getName()); + } +} \ No newline at end of file From 17e1132c1589b6cc7b7929c74830cccb57fc4dd7 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 29 Oct 2025 23:34:54 +0300 Subject: [PATCH 23/35] test: Wrote tests for AbstractStorage. Tests uses their own TestStorage and TestEntity. --- .../storage/AbstractStorageTest.java | 122 ++++++++++++++++++ .../filmorate/storage/TestEntity.java | 12 ++ .../filmorate/storage/TestStorage.java | 5 + 3 files changed, 139 insertions(+) create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/AbstractStorageTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/TestEntity.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/TestStorage.java diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/AbstractStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/AbstractStorageTest.java new file mode 100644 index 0000000..1d37493 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/AbstractStorageTest.java @@ -0,0 +1,122 @@ +package ru.yandex.practicum.filmorate.storage; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import ru.yandex.practicum.filmorate.exception.NotFoundException; + +import static org.junit.jupiter.api.Assertions.*; + +public class AbstractStorageTest { + + private TestStorage storage; + private TestEntity entity; + + @BeforeEach + public void setUp() { + storage = new TestStorage(); + entity = new TestEntity(); + } + + @Test + public void shouldAddObject() { + TestEntity entity1 = storage.add(entity); + + assertEquals(1L, entity1.getId()); + assertEquals(entity, entity1); + } + + @Test + public void shouldUpdateObject() { + storage.add(entity); + + TestEntity entity1 = storage.update(new TestEntity(1L)); + + assertEquals(1L, entity1.getId()); + assertEquals(entity, entity1); + } + + @Test + public void shouldThrowNotFoundWhenUpdateObjectThatDoesNotExist() { + assertThrows(NotFoundException.class, () -> storage.update(new TestEntity(1L))); + } + + @Test + public void shouldRemoveObject() { + storage.add(entity); + + TestEntity entity1 = storage.remove(1L); + + assertEquals(1L, entity1.getId()); + assertEquals(entity, entity1); + assertEquals(0, storage.getAll().size()); + } + + @Test + public void shouldThrowNotFoundWhenRemoveObjectThatDoesNotExist() { + assertThrows(NotFoundException.class, () -> storage.remove(1L)); + } + + @Test + public void shouldRemoveAll() { + storage.add(entity); + storage.clear(); + + assertEquals(0, storage.getAll().size()); + } + + @Test + public void shouldGetObject() { + storage.add(entity); + TestEntity entity1 = storage.get(1L); + + assertEquals(1L, entity1.getId()); + assertEquals(entity, entity1); + } + + @Test + public void shouldThrowNotFoundWhenGetObjectThatDoesNotExist() { + assertThrows(NotFoundException.class, () -> storage.get(1L)); + } + + @Test + public void shouldReturnAll() { + TestEntity t1 = new TestEntity(); + TestEntity t2 = new TestEntity(); + + storage.add(t1); + storage.add(t2); + + storage.clear(); + + assertEquals(0, storage.getAll().size()); + } + + @Test + public void shouldReturnTrueIfContains() { + storage.add(entity); + assertTrue(storage.contains(1L)); + } + + @Test + public void shouldReturnFalseIfNotContains() { + assertFalse(storage.contains(1L)); + } + + @Test + public void shouldReturnSize() { + storage.add(entity); + assertEquals(storage.getAll().size(), storage.size()); + } + + @Test + public void shouldThrowNotFoundIfNotContains() { + assertThrows(NotFoundException.class, () -> storage.throwIfNotFound(1L)); + } + + @Test + public void shouldNotThrowNotFoundIfContains() { + storage.add(entity); + assertDoesNotThrow(() -> storage.throwIfNotFound(1L)); + } + +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/TestEntity.java b/src/test/java/ru/yandex/practicum/filmorate/storage/TestEntity.java new file mode 100644 index 0000000..6662d6b --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/TestEntity.java @@ -0,0 +1,12 @@ +package ru.yandex.practicum.filmorate.storage; + +import lombok.NoArgsConstructor; +import ru.yandex.practicum.filmorate.model.StorageData; + +@NoArgsConstructor +public class TestEntity extends StorageData { + + public TestEntity(Long id) { + super(id); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/TestStorage.java b/src/test/java/ru/yandex/practicum/filmorate/storage/TestStorage.java new file mode 100644 index 0000000..e6a66fe --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/TestStorage.java @@ -0,0 +1,5 @@ +package ru.yandex.practicum.filmorate.storage; + +public class TestStorage extends AbstractStorage { +} + From 7b70e5f910fae0fa51181cb83175d1d9f7fff1a5 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 29 Oct 2025 23:35:07 +0300 Subject: [PATCH 24/35] test: Wrote tests for InMemoryStorages. --- .../storage/film/InMemoryFilmStorageTest.java | 176 ++++++++++++++++++ .../storage/user/InMemoryUserStorageTest.java | 175 +++++++++++++++++ 2 files changed, 351 insertions(+) create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorageTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorageTest.java diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorageTest.java new file mode 100644 index 0000000..7be4d80 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorageTest.java @@ -0,0 +1,176 @@ +package ru.yandex.practicum.filmorate.storage.film; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Film; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class InMemoryFilmStorageTest { + + private FilmStorage storage; + private Film film; + + @BeforeEach + public void setUp() { + storage = new InMemoryFilmStorage(); + film = new Film(); + } + + private Map> getLikesField() { + + try { + Field field = storage.getClass().getDeclaredField("likes"); + field.setAccessible(true); + return (Map>) field.get(storage); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + @Test + public void shouldAddFilmAndCreateLikesSet() { + Film user1 = storage.add(film); + + assertEquals(1L, user1.getId()); + assertEquals(film, user1); + assertNotNull(getLikesField().get(1L)); + } + + @Test + public void shouldRemoveFilmAndDeleteLikes() { + storage.add(film); + + Film user1 = storage.remove(1L); + + assertEquals(1L, user1.getId()); + assertEquals(film, user1); + assertEquals(0, storage.getAll().size()); + + assertNull(getLikesField().get(1L)); + } + + @Test + public void shouldThrowNotFoundWhenRemoveFilmThatDoesNotExist() { + assertThrows(NotFoundException.class, () -> storage.remove(1L)); + } + + @Test + public void shouldRemoveAllAndClearLikes() { + storage.add(film); + storage.clear(); + + assertEquals(0, storage.getAll().size()); + assertEquals(0, getLikesField().size()); + } + + @Test + public void shouldAddLike() { + storage.add(film); + + storage.addLike(1L, 1L); + storage.addLike(1L, 2L); + + assertEquals(2, film.getLikes()); + assertEquals(2, getLikesField().get(1L).size()); + } + + @Test + public void shouldAddLikeOnlyOnce() { + storage.add(film); + + storage.addLike(1L, 1L); + storage.addLike(1L, 1L); + + assertEquals(1, film.getLikes()); + assertEquals(1, getLikesField().get(1L).size()); + } + + @Test + public void shouldThrowNotFoundWhenAddLikeToFilmThatDoesNotExist() { + assertThrows(NotFoundException.class, () -> storage.addLike(1L, 1L)); + } + + @Test + public void shouldRemoveLike() { + storage.add(film); + + storage.addLike(1L, 1L); + storage.addLike(1L, 2L); + + storage.removeLike(1L, 1L); + + assertEquals(1, film.getLikes()); + assertEquals(1, getLikesField().get(1L).size()); + } + + @Test + public void shouldRemoveLikeOnlyOnce() { + storage.add(film); + + storage.addLike(1L, 1L); + storage.addLike(1L, 2L); + + storage.removeLike(1L, 1L); + storage.removeLike(1L, 1L); + + assertEquals(1, film.getLikes()); + assertEquals(1, getLikesField().get(1L).size()); + } + + @Test + public void shouldThrowNotFoundWhenRemoveLikeToFilmThatDoesNotExist() { + assertThrows(NotFoundException.class, () -> storage.removeLike(1L, 1L)); + } + + @Test + public void shouldReturnTopFilmsInOrder() { + storage.add(film); + storage.add(new Film()); + storage.add(new Film()); + + storage.addLike(3L, 1L); + storage.addLike(3L, 2L); + storage.addLike(3L, 3L); + + storage.addLike(2L, 1L); + storage.addLike(2L, 2L); + + storage.addLike(1L, 1L); + + + List sortedFilms = List.copyOf(storage.getTopFilms(10L)); + assertEquals(3L, sortedFilms.get(0).getId()); + assertEquals(2L, sortedFilms.get(1).getId()); + assertEquals(1L, sortedFilms.get(2).getId()); + } + + @Test + public void shouldReturnOnlyTwoTopFilmsInOrder() { + storage.add(film); + storage.add(new Film()); + storage.add(new Film()); + + storage.addLike(3L, 1L); + storage.addLike(3L, 2L); + storage.addLike(3L, 3L); + + storage.addLike(2L, 1L); + storage.addLike(2L, 2L); + + storage.addLike(1L, 1L); + + + List sortedFilms = List.copyOf(storage.getTopFilms(2L)); + assertEquals(2, sortedFilms.size()); + assertEquals(3L, sortedFilms.get(0).getId()); + assertEquals(2L, sortedFilms.get(1).getId()); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorageTest.java new file mode 100644 index 0000000..2d236a1 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorageTest.java @@ -0,0 +1,175 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.User; + +import java.lang.reflect.Field; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class InMemoryUserStorageTest { + + private UserStorage storage; + private User user; + + @BeforeEach + public void setUp() { + storage = new InMemoryUserStorage(); + user = new User(); + } + + private Map> getFriendShips() { + + try { + Field field = storage.getClass().getDeclaredField("friendships"); + field.setAccessible(true); + return (Map>) field.get(storage); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + @Test + public void shouldAddUserAndCreateFriendsMap() { + User user1 = storage.add(user); + + assertEquals(1L, user1.getId()); + assertEquals(user, user1); + assertNotNull(getFriendShips().get(1L)); + } + + @Test + public void shouldRemoveUserAndDeleteFriendShips() { + storage.add(user); + storage.add(new User()); + storage.add(new User()); + + storage.addFriend(1L, 2L); + storage.addFriend(1L, 3L); + + User user1 = storage.remove(1L); + + + assertEquals(1L, user1.getId()); + assertEquals(user, user1); + assertEquals(2, storage.getAll().size()); + + assertFalse(getFriendShips().containsKey(1L)); + assertFalse(getFriendShips().get(2L).containsKey(1L)); + assertFalse(getFriendShips().get(3L).containsKey(1L)); + } + + @Test + public void shouldThrowNotFoundWhenRemoveUserThatDoesNotExist() { + assertThrows(NotFoundException.class, () -> storage.remove(1L)); + } + + @Test + public void shouldRemoveAllAndClearFriends() { + storage.add(user); + storage.clear(); + + assertEquals(0, storage.getAll().size()); + assertEquals(0, getFriendShips().size()); + } + + @Test + public void shouldCreateFriendShip() { + storage.add(user); + storage.add(new User()); + + storage.addFriend(1L, 2L); + + assertTrue(getFriendShips().get(1L).containsKey(2L)); + assertTrue(getFriendShips().get(2L).containsKey(1L)); + } + + @Test + public void shouldThrowNotFoundWhenSenderIsNotFoundCreateFriendShip() { + storage.add(user); + + assertThrows(NotFoundException.class, () -> storage.addFriend(1000L, 1L)); + } + + @Test + public void shouldThrowNotFoundWhenReceiverIsNotFoundCreateFriendShip() { + storage.add(user); + + assertThrows(NotFoundException.class, () -> storage.addFriend(1L, 1000L)); + } + + @Test + public void shouldBreakFriendShip() { + storage.add(user); + storage.add(new User()); + + storage.addFriend(1L, 2L); + storage.deleteFriend(1L, 2L); + + assertFalse(getFriendShips().get(1L).containsKey(2L)); + assertFalse(getFriendShips().get(2L).containsKey(1L)); + } + + @Test + public void shouldThrowNotFoundWhenSenderIsNotFoundBreakFriendShip() { + storage.add(user); + + assertThrows(NotFoundException.class, () -> storage.deleteFriend(1000L, 1L)); + } + + @Test + public void shouldThrowNotFoundWhenReceiverIsNotFoundBreakFriendShip() { + storage.add(user); + + assertThrows(NotFoundException.class, () -> storage.deleteFriend(1L, 1000L)); + } + + @Test + public void shouldReturnFriends() { + storage.add(user); + storage.add(new User()); + + storage.addFriend(1L, 2L); + + assertTrue(storage.getFriends(2L).contains(user)); + } + + @Test + public void shouldThrowNotFoundWhenUserIsNotFoundReturnFriends() { + assertThrows(NotFoundException.class, () -> storage.getFriends(1000L)); + } + + @Test + public void shouldReturnCommonFriends() { + storage.add(user); + User user2 = storage.add(new User()); + User user3 = storage.add(new User()); + + storage.addFriend(1L, 2L); + storage.addFriend(2L, 3L); + + assertTrue(storage.getCommonFriends(1L, 3L).contains(user2)); + assertFalse(storage.getCommonFriends(1L, 3L).contains(user)); + assertFalse(storage.getCommonFriends(1L, 3L).contains(user3)); + + assertTrue(storage.getCommonFriends(1L, 2L).isEmpty()); + assertTrue(storage.getCommonFriends(2L, 3L).isEmpty()); + } + + @Test + public void shouldThrowNotFoundWhenSenderIsNotFoundReturnCommonFriends() { + assertThrows(NotFoundException.class, () -> storage.getCommonFriends(1000L, 1L)); + } + + @Test + public void shouldThrowNotFoundWhenReceiverIsNotFoundReturnCommonFriends() { + storage.add(user); + + assertThrows(NotFoundException.class, () -> storage.getCommonFriends(1L, 1000L)); + } + +} \ No newline at end of file From 4aea8c5eb6e1933af1a358b5b744562555fd0407 Mon Sep 17 00:00:00 2001 From: Crodi Date: Sat, 1 Nov 2025 06:01:03 +0300 Subject: [PATCH 25/35] fix: deleted AbstractStorage.java and all classes connected to it. --- .../filmorate/model/StorageData.java | 13 -- .../filmorate/storage/AbstractStorage.java | 79 ------------ .../storage/AbstractStorageTest.java | 122 ------------------ .../filmorate/storage/TestEntity.java | 12 -- .../filmorate/storage/TestStorage.java | 5 - 5 files changed, 231 deletions(-) delete mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/StorageData.java delete mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/AbstractStorage.java delete mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/AbstractStorageTest.java delete mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/TestEntity.java delete mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/TestStorage.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/StorageData.java b/src/main/java/ru/yandex/practicum/filmorate/model/StorageData.java deleted file mode 100644 index 3b96e38..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/model/StorageData.java +++ /dev/null @@ -1,13 +0,0 @@ -package ru.yandex.practicum.filmorate.model; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public abstract class StorageData { - protected Long id; -} - diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/AbstractStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/AbstractStorage.java deleted file mode 100644 index d489d99..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/AbstractStorage.java +++ /dev/null @@ -1,79 +0,0 @@ -package ru.yandex.practicum.filmorate.storage; - -import ru.yandex.practicum.filmorate.exception.NotFoundException; -import ru.yandex.practicum.filmorate.model.StorageData; - -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; - -public abstract class AbstractStorage implements BasicStorage { - - private final Map storage; - private final AtomicLong idGenerator; - - public AbstractStorage() { - this.storage = new ConcurrentHashMap<>(); - idGenerator = new AtomicLong(1); - } - - @Override - public T add(T value) { - value.setId(idGenerator.getAndIncrement()); - storage.put(value.getId(), value); - - return storage.get(value.getId()); - } - - @Override - public T update(T newUser) { - throwIfNotFound(newUser.getId()); - - storage.put(newUser.getId(), newUser); - - return storage.get(newUser.getId()); - } - - @Override - public T remove(Long id) { - throwIfNotFound(id); - - return storage.remove(id); - } - - @Override - public void clear() { - storage.clear(); - } - - @Override - public T get(Long id) { - throwIfNotFound(id); - - return storage.get(id); - } - - @Override - public Collection getAll() { - return List.copyOf(storage.values()); - } - - @Override - public boolean contains(Long id) { - return storage.containsKey(id); - } - - @Override - public int size() { - return storage.size(); - } - - @Override - public void throwIfNotFound(Long id) { - if (!contains(id)) { - throw new NotFoundException("Сущность с id = " + id + " не найдена"); - } - } -} diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/AbstractStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/AbstractStorageTest.java deleted file mode 100644 index 1d37493..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/AbstractStorageTest.java +++ /dev/null @@ -1,122 +0,0 @@ -package ru.yandex.practicum.filmorate.storage; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import ru.yandex.practicum.filmorate.exception.NotFoundException; - -import static org.junit.jupiter.api.Assertions.*; - -public class AbstractStorageTest { - - private TestStorage storage; - private TestEntity entity; - - @BeforeEach - public void setUp() { - storage = new TestStorage(); - entity = new TestEntity(); - } - - @Test - public void shouldAddObject() { - TestEntity entity1 = storage.add(entity); - - assertEquals(1L, entity1.getId()); - assertEquals(entity, entity1); - } - - @Test - public void shouldUpdateObject() { - storage.add(entity); - - TestEntity entity1 = storage.update(new TestEntity(1L)); - - assertEquals(1L, entity1.getId()); - assertEquals(entity, entity1); - } - - @Test - public void shouldThrowNotFoundWhenUpdateObjectThatDoesNotExist() { - assertThrows(NotFoundException.class, () -> storage.update(new TestEntity(1L))); - } - - @Test - public void shouldRemoveObject() { - storage.add(entity); - - TestEntity entity1 = storage.remove(1L); - - assertEquals(1L, entity1.getId()); - assertEquals(entity, entity1); - assertEquals(0, storage.getAll().size()); - } - - @Test - public void shouldThrowNotFoundWhenRemoveObjectThatDoesNotExist() { - assertThrows(NotFoundException.class, () -> storage.remove(1L)); - } - - @Test - public void shouldRemoveAll() { - storage.add(entity); - storage.clear(); - - assertEquals(0, storage.getAll().size()); - } - - @Test - public void shouldGetObject() { - storage.add(entity); - TestEntity entity1 = storage.get(1L); - - assertEquals(1L, entity1.getId()); - assertEquals(entity, entity1); - } - - @Test - public void shouldThrowNotFoundWhenGetObjectThatDoesNotExist() { - assertThrows(NotFoundException.class, () -> storage.get(1L)); - } - - @Test - public void shouldReturnAll() { - TestEntity t1 = new TestEntity(); - TestEntity t2 = new TestEntity(); - - storage.add(t1); - storage.add(t2); - - storage.clear(); - - assertEquals(0, storage.getAll().size()); - } - - @Test - public void shouldReturnTrueIfContains() { - storage.add(entity); - assertTrue(storage.contains(1L)); - } - - @Test - public void shouldReturnFalseIfNotContains() { - assertFalse(storage.contains(1L)); - } - - @Test - public void shouldReturnSize() { - storage.add(entity); - assertEquals(storage.getAll().size(), storage.size()); - } - - @Test - public void shouldThrowNotFoundIfNotContains() { - assertThrows(NotFoundException.class, () -> storage.throwIfNotFound(1L)); - } - - @Test - public void shouldNotThrowNotFoundIfContains() { - storage.add(entity); - assertDoesNotThrow(() -> storage.throwIfNotFound(1L)); - } - -} diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/TestEntity.java b/src/test/java/ru/yandex/practicum/filmorate/storage/TestEntity.java deleted file mode 100644 index 6662d6b..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/TestEntity.java +++ /dev/null @@ -1,12 +0,0 @@ -package ru.yandex.practicum.filmorate.storage; - -import lombok.NoArgsConstructor; -import ru.yandex.practicum.filmorate.model.StorageData; - -@NoArgsConstructor -public class TestEntity extends StorageData { - - public TestEntity(Long id) { - super(id); - } -} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/TestStorage.java b/src/test/java/ru/yandex/practicum/filmorate/storage/TestStorage.java deleted file mode 100644 index e6a66fe..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/TestStorage.java +++ /dev/null @@ -1,5 +0,0 @@ -package ru.yandex.practicum.filmorate.storage; - -public class TestStorage extends AbstractStorage { -} - From 1a5c31af50dcc9df9337c9d8391bea0ae17e0953 Mon Sep 17 00:00:00 2001 From: Crodi Date: Sat, 1 Nov 2025 06:01:16 +0300 Subject: [PATCH 26/35] fix: deleted Like and Friend services --- .../service/film/FilmLikeService.java | 77 ---------------- .../service/user/FriendShipService.java | 89 ------------------- 2 files changed, 166 deletions(-) delete mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/film/FilmLikeService.java delete mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/user/FriendShipService.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmLikeService.java b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmLikeService.java deleted file mode 100644 index c50a6af..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmLikeService.java +++ /dev/null @@ -1,77 +0,0 @@ -package ru.yandex.practicum.filmorate.service.film; - -import org.springframework.stereotype.Service; -import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.storage.film.FilmStorage; -import ru.yandex.practicum.filmorate.storage.user.UserStorage; - -import java.util.Collection; - -/** - * Service for managing film likes functionality. - *

    - * Handles operations related to film likes including adding, removing likes and retrieving top-liked films. - * Maintains an internal concurrent storage for tracking user likes for each film. - *

    - * - * @see Film - * @see FilmStorage - * @see UserStorage - */ -@Service -public class FilmLikeService { - - private final FilmStorage filmStorage; - private final UserStorage userStorage; - - /** - * Constructor for dependency injection - */ - public FilmLikeService(FilmStorage filmStorage, UserStorage userStorage) { - this.filmStorage = filmStorage; - this.userStorage = userStorage; - } - - /** - * Adds like to film. - *

    Adds userId in set of ids for specific film. - * Sets likes value in Film to set size. - * - * @param filmId film`s id to find set of likes. - * @param userId user`s id to add to set of likes. - * @return Film where like was added. - * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user or films is not present in storage. - */ - public Film addLike(Long filmId, Long userId) { - userStorage.throwIfNotFound(userId); - - return filmStorage.addLike(filmId, userId); - } - - /** - * Remove like from film. - *

    Remove userId from set of ids for specific film. - * Sets likes value in Film to set size. - * - * @param filmId film`s id to find set of likes. - * @param userId user`s id to delete from set of likes. - * @return Film where like was deleted. - * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user or films is not present in storage. - */ - public Film removeLike(Long filmId, Long userId) { - userStorage.throwIfNotFound(userId); - - return filmStorage.removeLike(filmId, userId); - } - - /** - * Return top-liked films with limit. - *

    Streams through all films and sorts them by their likes amount. - * - * @param count limit to returned collection. - * @return collection of Films. - */ - public Collection getTopFilms(Long count) { - return filmStorage.getTopFilms(count); - } -} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/user/FriendShipService.java b/src/main/java/ru/yandex/practicum/filmorate/service/user/FriendShipService.java deleted file mode 100644 index 158aaf4..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/service/user/FriendShipService.java +++ /dev/null @@ -1,89 +0,0 @@ -package ru.yandex.practicum.filmorate.service.user; - -import org.springframework.stereotype.Service; -import ru.yandex.practicum.filmorate.exception.ValidationException; -import ru.yandex.practicum.filmorate.model.User; -import ru.yandex.practicum.filmorate.storage.user.UserStorage; - -import java.util.Collection; -import java.util.Objects; - -/** - * Service for managing user friendships and social connections. - *

    - * Handles friendship operations including adding/removing friends, retrieving friend lists, - * and finding common friends between users. Maintains bidirectional friendship relationships. - *

    - * - * @see User - * @see UserStorage - */ -@Service -public class FriendShipService { - - private final UserStorage userStorage; - - /** - * Constructor for dependency injection - */ - public FriendShipService(UserStorage userStorage) { - this.userStorage = userStorage; - } - - /** - * Makes connection(friendship) between two users. - *
  • Sender adds Receiver to his friend map. - *
  • Receiver adds Sender to his friend map. - * - * @param senderId ID of the user that sends friendship request. Must be positive and exist in the system. - * @param receiverId ID of the user that gets friendship request. Must be positive and exist in the system. - * @return senderId`s collection of friends - * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user is not found - * @throws ru.yandex.practicum.filmorate.exception.ValidationException if senderId equals receiverId - */ - public Collection addFriend(Long senderId, Long receiverId) { - if (Objects.equals(receiverId, senderId)) { - throw new ValidationException("Сам себя не добавишь - никто не добавит"); - } - - return userStorage.addFriend(senderId, receiverId); - } - - /** - * Breaks connection(friendship) between two users. - *
  • Sender removes Receiver to his friend map. - *
  • Receiver removes Sender to his friend map. - * - * @param senderId ID of the user from where friend should be deleted. Must be positive and exist in the system. - * @param receiverId ID of the user that is being deleted. Must be positive and exist in the system. - * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user is not found - */ - public void deleteFriend(Long senderId, Long receiverId) { - userStorage.deleteFriend(senderId, receiverId); - } - - /** - * Returns user`s friends. - * - * @param id ID of the user to get friends. Must be positive and exist in the system. - * @return Users`s collection of friends - * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user is not found - */ - public Collection getFriends(Long id) { - return userStorage.getFriends(id); - } - - /** - * Finds intersection of friends between two users. - *

    Example: If user1 has friends [A, B, C] and user2 has friends [B, C, D], - * returns [B, C]. - * - * @param id first user ID. Must be positive and exist in the system. - * @param otherId second user ID. Must be positive and exist in the system. - * @return common friends collection. - * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user is not found - */ - public Collection getCommonFriends(Long id, Long otherId) { - return userStorage.getCommonFriends(id, otherId); - } -} From 8c983b4b4c316861f59a08cf9a926d670fae0f4a Mon Sep 17 00:00:00 2001 From: Crodi Date: Sat, 1 Nov 2025 06:02:00 +0300 Subject: [PATCH 27/35] fix: reworked storage interfaces --- .../practicum/filmorate/storage/BasicStorage.java | 7 +------ .../practicum/filmorate/storage/film/FilmStorage.java | 8 -------- .../practicum/filmorate/storage/user/UserStorage.java | 10 ---------- 3 files changed, 1 insertion(+), 24 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/BasicStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/BasicStorage.java index c327f8b..0d5f7d7 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/BasicStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/BasicStorage.java @@ -1,10 +1,8 @@ package ru.yandex.practicum.filmorate.storage; -import ru.yandex.practicum.filmorate.model.StorageData; - import java.util.Collection; -public interface BasicStorage { +public interface BasicStorage { T add(T t); @@ -12,8 +10,6 @@ public interface BasicStorage { T remove(Long id); - void clear(); - T get(Long id); Collection getAll(); @@ -22,5 +18,4 @@ public interface BasicStorage { int size(); - void throwIfNotFound(Long id); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java index 4224c22..6e55005 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java @@ -3,13 +3,5 @@ import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.storage.BasicStorage; -import java.util.Collection; - public interface FilmStorage extends BasicStorage { - - Film addLike(Long filmId, Long userId); - - Film removeLike(Long filmId, Long userId); - - Collection getTopFilms(Long count); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java index f72cde7..ae4c687 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java @@ -3,15 +3,5 @@ import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.storage.BasicStorage; -import java.util.Collection; - public interface UserStorage extends BasicStorage { - - Collection addFriend(Long senderId, Long receiverId); - - void deleteFriend(Long senderId, Long receiverId); - - Collection getFriends(Long id); - - Collection getCommonFriends(Long id, Long otherId); } From 7d40e5b1cda93431087b9092772faa4f5ab732dc Mon Sep 17 00:00:00 2001 From: Crodi Date: Sat, 1 Nov 2025 06:02:34 +0300 Subject: [PATCH 28/35] feat: add FriendShipStorage as main storage for friendship connections --- .../storage/user/FriendShipStorage.java | 16 +++++++ .../user/InMemoryFriendShipStorage.java | 43 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/user/FriendShipStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryFriendShipStorage.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/FriendShipStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/FriendShipStorage.java new file mode 100644 index 0000000..d54df19 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/FriendShipStorage.java @@ -0,0 +1,16 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import java.util.Set; + +public interface FriendShipStorage { + + void initializeFriendsSet(Long id); + + void clearFriendsSet(Long id); + + void addFriend(Long senderId, Long receiverId); + + void deleteFriend(Long senderId, Long receiverId); + + Set getFriends(Long id); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryFriendShipStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryFriendShipStorage.java new file mode 100644 index 0000000..88cb81d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryFriendShipStorage.java @@ -0,0 +1,43 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import org.springframework.stereotype.Component; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class InMemoryFriendShipStorage implements FriendShipStorage { + + private final Map> friendships; + + public InMemoryFriendShipStorage() { + this.friendships = new ConcurrentHashMap<>(); + } + + @Override + public void initializeFriendsSet(Long id) { + friendships.put(id, new HashSet<>()); + } + + @Override + public void clearFriendsSet(Long id) { + friendships.remove(id); + } + + @Override + public void addFriend(Long senderId, Long receiverId) { + friendships.get(senderId).add(receiverId); + } + + @Override + public void deleteFriend(Long senderId, Long receiverId) { + friendships.get(senderId).remove(receiverId); + } + + @Override + public Set getFriends(Long id) { + return friendships.get(id); + } +} From c8209cafd8ad4c0f63d592125cea7610688e15f4 Mon Sep 17 00:00:00 2001 From: Crodi Date: Sat, 1 Nov 2025 06:02:54 +0300 Subject: [PATCH 29/35] feat: add LikeStorage as main storage for likes. --- .../storage/film/InMemoryLikeStorage.java | 42 +++++++++++++++++++ .../filmorate/storage/film/LikeStorage.java | 12 ++++++ 2 files changed, 54 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryLikeStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/film/LikeStorage.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryLikeStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryLikeStorage.java new file mode 100644 index 0000000..5cf8a84 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryLikeStorage.java @@ -0,0 +1,42 @@ +package ru.yandex.practicum.filmorate.storage.film; + +import org.springframework.stereotype.Component; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class InMemoryLikeStorage implements LikeStorage { + + private final Map> likes; + + public InMemoryLikeStorage() { + this.likes = new ConcurrentHashMap<>(); + } + + @Override + public void initializeLikesSet(Long id) { + likes.put(id, new HashSet<>()); + } + + @Override + public void clearLikesSet(Long id) { + likes.remove(id); + } + + @Override + public Long addLike(Long filmId, Long userId) { + likes.get(filmId).add(userId); + + return (long) likes.get(filmId).size(); + } + + @Override + public Long deleteLike(Long filmId, Long userId) { + likes.get(filmId).remove(userId); + + return (long) likes.get(filmId).size(); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/LikeStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/LikeStorage.java new file mode 100644 index 0000000..c2cc79e --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/LikeStorage.java @@ -0,0 +1,12 @@ +package ru.yandex.practicum.filmorate.storage.film; + +public interface LikeStorage { + + void initializeLikesSet(Long id); + + void clearLikesSet(Long id); + + Long addLike(Long filmId, Long userId); + + Long deleteLike(Long filmId, Long userId); +} From 921c5c6487c0746744c8fc858555a0aea34fa5cb Mon Sep 17 00:00:00 2001 From: Crodi Date: Sat, 1 Nov 2025 06:03:44 +0300 Subject: [PATCH 30/35] fix: reworked storage with solid principles. --- .../storage/film/InMemoryFilmStorage.java | 72 ++++++---------- .../storage/user/InMemoryUserStorage.java | 85 ++++++------------- 2 files changed, 54 insertions(+), 103 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java index cf44e38..82b593c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java @@ -1,89 +1,71 @@ -package ru.yandex.practicum.filmorate.storage.film; +package ru.yandex.practicum.filmorate.films.film; 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.AbstractStorage; +import ru.yandex.practicum.filmorate.storage.film.FilmStorage; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; /** - * In-memory implementation of film storage. + * In-memory implementation of film films. *

    - * Provides thread-safe storage for Film objects using ConcurrentHashMap with atomic ID generation. - * Suitable for development and testing environments without persistent storage requirements. + * Provides thread-safe films for Film objects using ConcurrentHashMap with atomic ID generation. + * Suitable for development and testing environments without persistent films requirements. *

    * * @see Film * @see FilmStorage */ @Component -public class InMemoryFilmStorage extends AbstractStorage implements FilmStorage { +public class InMemoryFilmStorage implements FilmStorage { - private final Map> likes; + private final Map films; + private final AtomicLong idGenerator; public InMemoryFilmStorage() { - super(); - likes = new ConcurrentHashMap<>(); + this.films = new ConcurrentHashMap<>(); + this.idGenerator = new AtomicLong(1); } @Override public Film add(Film film) { - Film returnFilm = super.add(film); - likes.put(film.getId(), new HashSet<>()); + film.setId(idGenerator.getAndIncrement()); + films.put(film.getId(), film); - return returnFilm; + return films.get(film.getId()); } @Override - public Film remove(Long id) { - Film film = super.remove(id); - likes.remove(film.getId()); + public Film update(Film newFilm) { + films.put(newFilm.getId(), newFilm); - return film; + return films.get(newFilm.getId()); } @Override - public void clear() { - super.clear(); - likes.clear(); + public Film remove(Long id) { + return films.remove(id); } @Override - public void throwIfNotFound(Long id) { - if (!contains(id)) { - throw new NotFoundException("Фильм с id = " + id + " не найден"); - } + public Film get(Long id) { + return films.get(id); } @Override - public Film addLike(Long filmId, Long userId) { - Film film = get(filmId); - - Set filmLikes = likes.get(filmId); - filmLikes.add(userId); - film.setLikes((long) filmLikes.size()); - - return film; + public Collection getAll() { + return List.copyOf(films.values()); } @Override - public Film removeLike(Long filmId, Long userId) { - Film film = get(filmId); - - Set set = likes.get(filmId); - set.remove(userId); - film.setLikes((long) set.size()); - - return film; + public boolean contains(Long id) { + return films.containsKey(id); } @Override - public Collection getTopFilms(Long count) { - return getAll().stream() - .sorted(Comparator.comparingLong(Film::getLikes).reversed()) - .limit(count) - .toList(); + public int size() { + return films.size(); } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java index 5a17864..4b26fae 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java @@ -1,103 +1,72 @@ package ru.yandex.practicum.filmorate.storage.user; 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.AbstractStorage; import java.util.Collection; +import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; /** - * In-memory implementation of user storage. + * In-memory implementation of user users. *

    - * Provides thread-safe storage for User objects using ConcurrentHashMap with atomic ID generation. - * Suitable for development and testing environments without persistent storage requirements. + * Provides thread-safe users for User objects using ConcurrentHashMap with atomic ID generation. + * Suitable for development and testing environments without persistent users requirements. *

    * * @see User * @see UserStorage */ @Component -public class InMemoryUserStorage extends AbstractStorage implements UserStorage { +public class InMemoryUserStorage implements UserStorage { - private final Map> friendships; + private final Map users; + private final AtomicLong idGenerator; public InMemoryUserStorage() { - super(); - friendships = new ConcurrentHashMap<>(); + this.users = new ConcurrentHashMap<>(); + this.idGenerator = new AtomicLong(1); } @Override public User add(User user) { - User returnUser = super.add(user); - friendships.put(user.getId(), new ConcurrentHashMap<>()); + user.setId(idGenerator.getAndIncrement()); + users.put(user.getId(), user); - return returnUser; + return users.get(user.getId()); } @Override - public User remove(Long id) { - throwIfNotFound(id); - - Set friendIds = friendships.get(id).keySet(); - friendIds.forEach(friendId -> friendships.get(friendId).remove(id)); - friendships.remove(id); + public User update(User newUser) { + users.put(newUser.getId(), newUser); - return super.remove(id); - } - - @Override - public void clear() { - super.clear(); - friendships.clear(); + return users.get(newUser.getId()); } @Override - public void throwIfNotFound(Long id) { - if (!contains(id)) { - throw new NotFoundException("Пользователь с id = " + id + " не найден"); - } + public User remove(Long id) { + return users.remove(id); } @Override - public Collection addFriend(Long senderId, Long receiverId) { - User receiver = get(receiverId); - User friend = get(senderId); - - friendships.get(receiverId).put(senderId, friend); - friendships.get(senderId).put(receiverId, receiver); - return friendships.get(senderId).values(); + public User get(Long id) { + return users.get(id); } @Override - public void deleteFriend(Long senderId, Long receiverId) { - throwIfNotFound(senderId); - throwIfNotFound(receiverId); - - friendships.get(senderId).remove(receiverId); - friendships.get(receiverId).remove(senderId); + public Collection getAll() { + return List.copyOf(users.values()); } @Override - public Collection getFriends(Long id) { - throwIfNotFound(id); - return friendships.get(id).values(); + public boolean contains(Long id) { + return users.containsKey(id); } @Override - public Collection getCommonFriends(Long id, Long otherId) { - throwIfNotFound(id); - throwIfNotFound(otherId); - - Set friends = friendships.get(id).keySet(); - Map otherFriends = friendships.get(otherId); - - return friends.stream() - .filter(otherFriends::containsKey) - .map(otherFriends::get) - .toList(); + public int size() { + return users.size(); } -} \ No newline at end of file +} From 2bd7c2a482401244966b2952536a345a2cd0820d Mon Sep 17 00:00:00 2001 From: Crodi Date: Sat, 1 Nov 2025 06:04:04 +0300 Subject: [PATCH 31/35] feat: add interfaces for services. --- .../service/film/FilmServiceInterface.java | 26 +++++++++++++++++ .../service/user/UserServiceInterface.java | 28 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceInterface.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/user/UserServiceInterface.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceInterface.java b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceInterface.java new file mode 100644 index 0000000..69bf087 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceInterface.java @@ -0,0 +1,26 @@ +package ru.yandex.practicum.filmorate.service.film; + +import ru.yandex.practicum.filmorate.model.Film; + +import java.util.Collection; + +public interface FilmServiceInterface { + + Film getFilm(Long id); + + Collection getAllFilms(); + + Film addFilm(Film film); + + Film updateFilm(Film film); + + void deleteFilm(Long id); + + Film addLike(Long filmId, Long userId); + + Film removeLike(Long filmId, Long userId); + + Collection getTopFilms(Long count); + + void throwIfNotFound(Long id); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/user/UserServiceInterface.java b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserServiceInterface.java new file mode 100644 index 0000000..6efaa94 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserServiceInterface.java @@ -0,0 +1,28 @@ +package ru.yandex.practicum.filmorate.service.user; + +import ru.yandex.practicum.filmorate.model.User; + +import java.util.Collection; + +public interface UserServiceInterface { + + User getUser(Long id); + + Collection getAllUsers(); + + User addUser(User user); + + User updateUser(User user); + + void deleteUser(Long id); + + void addFriend(Long senderId, Long receiverId); + + void deleteFriend(Long senderId, Long receiverId); + + Collection getFriends(Long id); + + Collection getCommonFriends(Long id, Long otherId); + + void throwIfNotFound(Long id); +} From d8ee6490faacae0a4d02446918447e2329dae865 Mon Sep 17 00:00:00 2001 From: Crodi Date: Sat, 1 Nov 2025 06:04:57 +0300 Subject: [PATCH 32/35] feat: reworked services, now business logic is only here. --- .../filmorate/service/film/FilmService.java | 75 +++++++++++-- .../filmorate/service/user/UserService.java | 100 ++++++++++++++---- 2 files changed, 146 insertions(+), 29 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java index f13d1aa..b53d904 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java @@ -1,11 +1,15 @@ package ru.yandex.practicum.filmorate.service.film; -import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.storage.film.FilmStorage; +import ru.yandex.practicum.filmorate.storage.film.LikeStorage; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; import java.util.Collection; +import java.util.Comparator; /** * Main service for film operations and business logic. @@ -16,21 +20,21 @@ * * @see Film * @see FilmStorage - * @see FilmLikeService */ @Service -@Slf4j -public class FilmService { +public class FilmService implements FilmServiceInterface { private final FilmStorage filmStorage; - private final FilmLikeService filmLikeService; + private final UserStorage userStorage; + private final LikeStorage likeStorage; /** * Constructor for dependency injection */ - public FilmService(FilmStorage filmStorage, FilmLikeService filmLikeService) { + public FilmService(FilmStorage filmStorage, UserStorage userStorage, LikeStorage likeStorage) { this.filmStorage = filmStorage; - this.filmLikeService = filmLikeService; + this.userStorage = userStorage; + this.likeStorage = likeStorage; } /** @@ -38,8 +42,11 @@ public FilmService(FilmStorage filmStorage, FilmLikeService filmLikeService) { * * @param id Film`s id to get. * @return Film. + * @throws NotFoundException if film not found */ + @Override public Film getFilm(Long id) { + throwIfNotFound(id); return filmStorage.get(id); } @@ -48,6 +55,7 @@ public Film getFilm(Long id) { * * @return Collection of all filmsStorage. */ + @Override public Collection getAllFilms() { return filmStorage.getAll(); } @@ -58,8 +66,11 @@ public Collection getAllFilms() { * @param film Film to add. * @return created Film. */ + @Override public Film addFilm(Film film) { - return filmStorage.add(film); + Film returnFilm = filmStorage.add(film); + likeStorage.initializeLikesSet(returnFilm.getId()); + return returnFilm; } /** @@ -67,8 +78,15 @@ public Film addFilm(Film film) { * * @param film Film to update. * @return updated Film. + * @throws NotFoundException if film not found */ + @Override public Film updateFilm(Film film) { + if (film.getId() == null) { + throw new ValidationException("Id должен быть указан"); + } + throwIfNotFound(film.getId()); + return filmStorage.update(film); } @@ -76,9 +94,13 @@ public Film updateFilm(Film film) { * Deletes film from the storage. * * @param id Film`s id to delete. + * @throws NotFoundException if film not found */ + @Override public void deleteFilm(Long id) { + throwIfNotFound(id); filmStorage.remove(id); + likeStorage.clearLikesSet(id); } /** @@ -87,9 +109,19 @@ public void deleteFilm(Long id) { * @param filmId Film`s id to add like. * @param userId User`s id to set like. * @return Film where likes where added. + * @throws NotFoundException if film or user not found */ + @Override public Film addLike(Long filmId, Long userId) { - return filmLikeService.addLike(filmId, userId); + throwIfNotFound(filmId); + if (!userStorage.contains(userId)) { + throw new NotFoundException("Пользователь с id = " + userId + " не найден"); + } + + Long newLikes = likeStorage.addLike(filmId, userId); + filmStorage.get(filmId).setLikes(newLikes); + + return filmStorage.get(filmId); } /** @@ -98,9 +130,19 @@ public Film addLike(Long filmId, Long userId) { * @param filmId Film`s id to delete like. * @param userId User`s id to delete like. * @return Film where likes where deleted. + * @throws NotFoundException if film or user not found */ + @Override public Film removeLike(Long filmId, Long userId) { - return filmLikeService.removeLike(filmId, userId); + throwIfNotFound(filmId); + if (!userStorage.contains(userId)) { + throw new NotFoundException("Пользователь с id = " + userId + " не найден"); + } + + Long newLikes = likeStorage.deleteLike(filmId, userId); + filmStorage.get(filmId).setLikes(newLikes); + + return filmStorage.get(filmId); } /** @@ -109,8 +151,19 @@ public Film removeLike(Long filmId, Long userId) { * @param count limit to collection. * @return collection of Films. */ + @Override public Collection getTopFilms(Long count) { - return filmLikeService.getTopFilms(count); + + return filmStorage.getAll().stream() + .sorted(Comparator.comparingLong(Film::getLikes).reversed()) + .limit(count) + .toList(); } + @Override + public void throwIfNotFound(Long id) { + if (!filmStorage.contains(id)) { + throw new NotFoundException("Фильм с id = " + id + " не найден"); + } + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java index 480569b..eff4749 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java @@ -1,10 +1,15 @@ package ru.yandex.practicum.filmorate.service.user; import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.user.FriendShipStorage; import ru.yandex.practicum.filmorate.storage.user.UserStorage; import java.util.Collection; +import java.util.Objects; +import java.util.Set; /** * Main service for user operations and business logic. @@ -15,34 +20,36 @@ * * @see User * @see UserStorage - * @see FriendShipService */ @Service -public class UserService { +public class UserService implements UserServiceInterface { private final UserStorage userStorage; - private final FriendShipService friendShipService; + private final FriendShipStorage friendShipStorage; /** * Constructor for dependency injection */ - public UserService(UserStorage userStorage, FriendShipService friendShipService) { + public UserService(UserStorage userStorage, FriendShipStorage friendShipStorage) { this.userStorage = userStorage; - this.friendShipService = friendShipService; + this.friendShipStorage = friendShipStorage; } /** * @param id ID of the user to be deleted. Must be positive and exist in the system. * @return User from storage. - * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user is not found. + * @throws NotFoundException if user is not found. */ + @Override public User getUser(Long id) { + throwIfNotFound(id); return userStorage.get(id); } /** * @return Collection of all users in storage. */ + @Override public Collection getAllUsers() { return userStorage.getAll(); } @@ -50,17 +57,22 @@ public Collection getAllUsers() { /** * Add user to the storage. *

    If user`s name is null, then login is set as a name. + *

    Initializes set for friends. * * @param user User to create. * @return created User. */ + @Override public User addUser(User user) { if (user.getName() == null || user.getName().isBlank()) { user.setName(user.getLogin()); } - return userStorage.add(user); + User returnUser = userStorage.add(user); + friendShipStorage.initializeFriendsSet(returnUser.getId()); + + return returnUser; } /** @@ -68,18 +80,34 @@ public User addUser(User user) { * * @param user User to replace. User`s id must be in the system. * @return updated User. - * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user is not found + * @throws NotFoundException if user is not found + * @throws ValidationException if ID is null */ + @Override public User updateUser(User user) { + if (user.getId() == null) { + throw new ValidationException("Id должен быть указан"); + } + throwIfNotFound(user.getId()); + return userStorage.update(user); } /** * Deletes user from the storage. + * Deletes user as from friends of all users. * * @param id ID of the user to be deleted. Must be positive and exist in the system. + * @throws NotFoundException if user not found */ + @Override public void deleteUser(Long id) { + throwIfNotFound(id); + + Set friendIds = friendShipStorage.getFriends(id); + friendIds.forEach(friendId -> friendShipStorage.getFriends(friendId).remove(id)); + friendShipStorage.clearFriendsSet(id); + userStorage.remove(id); } @@ -88,11 +116,20 @@ public void deleteUser(Long id) { * * @param senderId ID of the user that sends friendship request. Must be positive and exist in the system. * @param receiverId ID of the user that gets friendship request. Must be positive and exist in the system. - * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user is not found - * @throws ru.yandex.practicum.filmorate.exception.ValidationException if senderId equals receiverId + * @throws NotFoundException if user is not found + * @throws ValidationException if senderId equals receiverId */ - public Collection addFriend(Long senderId, Long receiverId) { - return friendShipService.addFriend(senderId, receiverId); + @Override + public void addFriend(Long senderId, Long receiverId) { + if (Objects.equals(receiverId, senderId)) { + throw new ValidationException("Сам себя не добавишь - никто не добавит"); + } + + throwIfNotFound(senderId); + throwIfNotFound(receiverId); + + friendShipStorage.addFriend(senderId, receiverId); + friendShipStorage.addFriend(receiverId, senderId); } /** @@ -101,19 +138,29 @@ public Collection addFriend(Long senderId, Long receiverId) { * * @param senderId ID of the user from where friend should be deleted. Must be positive and exist in the system. * @param receiverId ID of the user that is being deleted. Must be positive and exist in the system. - * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user is not found + * @throws NotFoundException if user is not found */ + @Override public void deleteFriend(Long senderId, Long receiverId) { - friendShipService.deleteFriend(senderId, receiverId); + throwIfNotFound(senderId); + throwIfNotFound(receiverId); + + friendShipStorage.deleteFriend(senderId, receiverId); + friendShipStorage.deleteFriend(receiverId, senderId); } /** * @param id ID of the user to get friends. Must be positive and exist in the system. * @return Collection of all user`s friends. - * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user is not found + * @throws NotFoundException if user is not found */ + @Override public Collection getFriends(Long id) { - return friendShipService.getFriends(id); + throwIfNotFound(id); + + return friendShipStorage.getFriends(id).stream() + .map(userStorage::get) + .toList(); } /** @@ -122,9 +169,26 @@ public Collection getFriends(Long id) { * @param id ID of the first user. Must be positive and exist in the system. * @param otherId ID of the second user. Must be positive and exist in the system. * @return Collection of all common friends between users. - * @throws ru.yandex.practicum.filmorate.exception.NotFoundException if user is not found + * @throws NotFoundException if user is not found */ + @Override public Collection getCommonFriends(Long id, Long otherId) { - return friendShipService.getCommonFriends(id, otherId); + throwIfNotFound(id); + throwIfNotFound(otherId); + + Set friends = friendShipStorage.getFriends(id); + Set otherFriends = friendShipStorage.getFriends(otherId); + + return friends.stream() + .filter(otherFriends::contains) + .map(userStorage::get) + .toList(); + } + + @Override + public void throwIfNotFound(Long id) { + if (!userStorage.contains(id)) { + throw new NotFoundException("Пользователь с id = " + id + " не найден"); + } } } From 168cf3c846df231d915223fb4c404abd163f4c89 Mon Sep 17 00:00:00 2001 From: Crodi Date: Sat, 1 Nov 2025 06:05:21 +0300 Subject: [PATCH 33/35] feat: removed inheritance --- .../yandex/practicum/filmorate/model/Film.java | 11 ++++++----- .../yandex/practicum/filmorate/model/User.java | 18 ++++++------------ 2 files changed, 12 insertions(+), 17 deletions(-) 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 c85ae82..6fe8c60 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -4,7 +4,8 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Size; -import lombok.*; +import lombok.Data; +import lombok.NoArgsConstructor; import ru.yandex.practicum.filmorate.validation.ValidReleaseDate; import java.time.LocalDate; @@ -26,19 +27,19 @@ */ @Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) @NoArgsConstructor -public class Film extends StorageData { +public class Film { public Film(Long id, String name, String description, LocalDate releaseDate, Integer duration) { - super(id); + this.id = id; this.name = name; this.description = description; this.releaseDate = releaseDate; this.duration = duration; } + private Long id; + @NotBlank(message = "Название не может быть пустым") private String name; diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/User.java b/src/main/java/ru/yandex/practicum/filmorate/model/User.java index 520fcf8..e48cf03 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/User.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java @@ -3,7 +3,9 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.PastOrPresent; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; import ru.yandex.practicum.filmorate.validation.ValidLogin; import java.time.LocalDate; @@ -22,19 +24,11 @@ * */ @Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) @NoArgsConstructor @AllArgsConstructor -public class User extends StorageData { - - public User(Long id, String email, String login, String name, LocalDate birthday) { - super(id); - this.email = email; - this.login = login; - this.name = name; - this.birthday = birthday; - } +public class User { + + private Long id; @NotBlank(message = "Почта не может быть пустой") @Email From 170a5c06533d5456f0f967417616735947f918de Mon Sep 17 00:00:00 2001 From: Crodi Date: Sat, 1 Nov 2025 06:05:48 +0300 Subject: [PATCH 34/35] feat: moved business logic to services --- .../filmorate/controller/FilmController.java | 6 +++--- .../filmorate/controller/UserController.java | 17 ++++++----------- 2 files changed, 9 insertions(+), 14 deletions(-) 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 e9081fc..e490cb5 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -7,7 +7,7 @@ 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.service.film.FilmService; +import ru.yandex.practicum.filmorate.service.film.FilmServiceInterface; import java.util.Collection; @@ -16,12 +16,12 @@ @Slf4j public class FilmController { - private final FilmService filmService; + private final FilmServiceInterface filmService; /** * Constructor for dependency injection */ - public FilmController(FilmService filmService) { + public FilmController(FilmServiceInterface filmService) { this.filmService = filmService; } diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java index dbcd635..975e081 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -8,7 +8,7 @@ import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.User; -import ru.yandex.practicum.filmorate.service.user.UserService; +import ru.yandex.practicum.filmorate.service.user.UserServiceInterface; import java.util.Collection; @@ -17,12 +17,12 @@ @Slf4j public class UserController { - private final UserService userService; + private final UserServiceInterface userService; /** * Constructor for dependency injection */ - public UserController(UserService userService) { + public UserController(UserServiceInterface userService) { this.userService = userService; } @@ -64,10 +64,6 @@ public User addUser(@Valid @RequestBody User user) { */ @PutMapping public User updateUser(@Valid @RequestBody User newUser) { - if (newUser.getId() == null) { - throw new ValidationException("Id должен быть указан"); - } - return userService.updateUser(newUser); } @@ -106,14 +102,13 @@ public Collection getCommonFriends(@PathVariable @Positive Long id, * * @param id user`s id. Sender. Must be positive number. * @param friendId friend`s id. Receiver. Must be positive number. - * @return collection of sender friends. * @throws NotFoundException if user is not found * @throws ValidationException if id equals friendId */ @PutMapping("/{id}/friends/{friendId}") - public Collection addFriend(@PathVariable @Positive Long id, - @PathVariable @Positive Long friendId) { - return userService.addFriend(id, friendId); + public void addFriend(@PathVariable @Positive Long id, + @PathVariable @Positive Long friendId) { + userService.addFriend(id, friendId); } /** From d24b53c892a22988c09f4cbe1b6cbf0ffbafcf73 Mon Sep 17 00:00:00 2001 From: Crodi Date: Sat, 1 Nov 2025 06:06:00 +0300 Subject: [PATCH 35/35] feat: add tests --- .../controller/UserControllerTest.java | 28 +- .../service/film/FilmLikeServiceTest.java | 120 ------ .../service/film/FilmServiceTest.java | 288 ++++++++++++++ .../service/user/FriendShipServiceTest.java | 64 ---- .../service/user/UserServiceTest.java | 357 +++++++++++++++++- .../storage/film/InMemoryFilmStorageTest.java | 143 ++----- .../storage/film/InMemoryLikeStorageTest.java | 99 +++++ .../user/InMemoryFriendShipStorageTest.java | 102 +++++ .../storage/user/InMemoryUserStorageTest.java | 127 ++----- 9 files changed, 891 insertions(+), 437 deletions(-) delete mode 100644 src/test/java/ru/yandex/practicum/filmorate/service/film/FilmLikeServiceTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/service/film/FilmServiceTest.java delete mode 100644 src/test/java/ru/yandex/practicum/filmorate/service/user/FriendShipServiceTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryLikeStorageTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryFriendShipStorageTest.java diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java index 9658123..7884b1a 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java @@ -6,6 +6,7 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.service.user.UserService; @@ -316,22 +317,23 @@ public void shouldUpdateUser() throws Exception { } @Test - public void shouldNotUpdateFilmWhenNoIdAndReturnBadRequest() throws Exception { + public void shouldNotUpdateUserWhenNoIdAndReturnBadRequest() throws Exception { User userToUpdate = new User(null, "email@mail.org", "login1", "name1", LocalDate.now()); String json = objectMapper.writeValueAsString(userToUpdate); + when(userService.updateUser(userToUpdate)).thenThrow(new ValidationException("")); mockMvc.perform(put("/users") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isBadRequest()); - verify(userService, never()).updateUser(any()); + verify(userService, times(1)).updateUser(any()); } @Test - public void shouldNotUpdateFilmWhenNotFound() throws Exception { + public void shouldNotUpdateUserWhenNotFound() throws Exception { User userToUpdate = new User(1000L, "email@mail.org", "login1", "name1", LocalDate.now()); when(userService.updateUser(any(User.class))) @@ -349,17 +351,8 @@ public void shouldNotUpdateFilmWhenNotFound() throws Exception { @Test public void shouldAddFriend() throws Exception { - User user1 = new User(1L, "email1", "login1", "name1", LocalDate.now()); - User user2 = new User(2L, "email2", "login2", "name2", LocalDate.now()); - - List list = List.of(user1, user2); - - when(userService.addFriend(any(), any())).thenReturn(list); - mockMvc.perform(put("/users/3/friends/2")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$").isArray()) - .andExpect(jsonPath("$.length()").value(2)); + .andExpect(status().isOk()); verify(userService, times(1)).addFriend(any(), any()); } @@ -367,9 +360,8 @@ public void shouldAddFriend() throws Exception { @Test public void shouldReturnNotFoundWhenSenderNotFound() throws Exception { - when(userService.addFriend(1000L, 1L)) - .thenThrow(new NotFoundException("Пользователь с id = 1000 не найден")); - + doThrow(new NotFoundException("Пользователь с id = 1000 не найден")) + .when(userService).addFriend(1000L, 1L); mockMvc.perform(put("/users/1000/friends/1")) .andExpect(status().isNotFound()); @@ -380,8 +372,8 @@ public void shouldReturnNotFoundWhenSenderNotFound() throws Exception { @Test public void shouldReturnNotFoundWhenReceiverNotFound() throws Exception { - when(userService.addFriend(1L, 1000L)) - .thenThrow(new NotFoundException("Пользователь с id = 1000 не найден")); + doThrow(new NotFoundException("Пользователь с id = 1000 не найден")) + .when(userService).addFriend(1L, 1000L); mockMvc.perform(put("/users/1/friends/1000")) diff --git a/src/test/java/ru/yandex/practicum/filmorate/service/film/FilmLikeServiceTest.java b/src/test/java/ru/yandex/practicum/filmorate/service/film/FilmLikeServiceTest.java deleted file mode 100644 index 7ce4e83..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/service/film/FilmLikeServiceTest.java +++ /dev/null @@ -1,120 +0,0 @@ -package ru.yandex.practicum.filmorate.service.film; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import ru.yandex.practicum.filmorate.exception.NotFoundException; -import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.storage.film.FilmStorage; -import ru.yandex.practicum.filmorate.storage.user.UserStorage; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class FilmLikeServiceTest { - - @Mock - private FilmStorage filmStorage; - - @Mock - private UserStorage userStorage; - - private FilmLikeService filmLikeService; - - @BeforeEach - void setUp() { - filmLikeService = new FilmLikeService(filmStorage, userStorage); - } - - @Test - void shouldAddLikeWhenUserExist() { - // входящая информация - Long filmId = 1L; - Long userId = 1L; - Film film = new Film(); - film.setId(filmId); - - // что мы должны получить - Film filmWithLike = new Film(); - filmWithLike.setId(filmId); - filmWithLike.setLikes(1L); - - // настраиваем поведение мока - doNothing().when(userStorage).throwIfNotFound(userId); - when(filmStorage.addLike(filmId, userId)).thenReturn(filmWithLike); - - Film result = filmLikeService.addLike(filmId, userId); - - // проверки, что метод вызвался - verify(userStorage).throwIfNotFound(userId); - verify(filmStorage).addLike(filmId, userId); - - // проверка, что значения равны - assertEquals(1, result.getLikes()); - } - - @Test - void shouldNotAddLikeAndThrowNotFoundExceptionWhenUserDoesNotExist() { - Long userId = 1000L; - Long filmId = 1L; - Film film = new Film(); - film.setId(filmId); - - // говорю моку, что ему нужно НЕ НАЙТИ юзера - doThrow(NotFoundException.class).when(userStorage).throwIfNotFound(userId); - - assertThrows(NotFoundException.class, () -> filmLikeService.addLike(filmId, userId)); - - verify(userStorage).throwIfNotFound(userId); - verify(filmStorage, never()).addLike(filmId, userId); - } - - @Test - void shouldRemoveLikeWhenUserExist() { - // входящая информация - Long filmId = 1L; - Long userId = 1L; - Film film = new Film(); - film.setId(filmId); - film.setLikes(1L); - - // что мы должны получить - Film filmWithLike = new Film(); - filmWithLike.setId(filmId); - filmWithLike.setLikes(0L); - - // настраиваем поведение мока - doNothing().when(userStorage).throwIfNotFound(userId); - when(filmStorage.removeLike(filmId, userId)).thenReturn(filmWithLike); - - Film result = filmLikeService.removeLike(filmId, userId); - - // проверки, что метод вызвался - verify(userStorage).throwIfNotFound(userId); - verify(filmStorage).removeLike(filmId, userId); - - // проверка, что значения равны - assertEquals(0, result.getLikes()); - } - - @Test - void shouldNotRemoveLikeAndThrowNotFoundExceptionWhenUserDoesNotExist() { - Long userId = 1000L; - Long filmId = 1L; - Film film = new Film(); - film.setId(filmId); - film.setLikes(1L); - - // говорю моку, что ему нужно НЕ НАЙТИ юзера - doThrow(NotFoundException.class).when(userStorage).throwIfNotFound(userId); - - assertThrows(NotFoundException.class, () -> filmLikeService.removeLike(filmId, userId)); - - verify(userStorage).throwIfNotFound(userId); - verify(filmStorage, never()).addLike(filmId, userId); - } -} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/service/film/FilmServiceTest.java b/src/test/java/ru/yandex/practicum/filmorate/service/film/FilmServiceTest.java new file mode 100644 index 0000000..d883d1a --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/service/film/FilmServiceTest.java @@ -0,0 +1,288 @@ +package ru.yandex.practicum.filmorate.service.film; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ValidationException; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.storage.film.FilmStorage; +import ru.yandex.practicum.filmorate.storage.film.LikeStorage; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class FilmServiceTest { + + @Mock + private FilmStorage filmStorage; + + @Mock + private LikeStorage likeStorage; + + @Mock + private UserStorage userStorage; + + private FilmService filmService; + + @BeforeEach + public void setUp() { + filmService = new FilmService(filmStorage, userStorage, likeStorage); + } + + @Test + public void shouldGetFilmSuccessfully() { + Long filmId = 1L; + Film expectedFilm = new Film(filmId, "Film Name", "Description", LocalDate.now(), 120); + + when(filmStorage.contains(filmId)).thenReturn(true); + when(filmStorage.get(filmId)).thenReturn(expectedFilm); + + Film actualFilm = filmService.getFilm(filmId); + + assertNotNull(actualFilm); + assertEquals(expectedFilm, actualFilm); + verify(filmStorage).contains(filmId); + verify(filmStorage).get(filmId); + } + + @Test + public void shouldThrowNotFoundExceptionWhenFilmNotExists() { + Long filmId = 999L; + + when(filmStorage.contains(filmId)).thenReturn(false); + + assertThrows(NotFoundException.class, () -> filmService.getFilm(filmId)); + verify(filmStorage, never()).get(filmId); + } + + @Test + public void shouldGetAllFilms() { + Film film1 = new Film(1L, "Film 1", "Desc 1", LocalDate.now(), 120); + Film film2 = new Film(2L, "Film 2", "Desc 2", LocalDate.now(), 150); + Collection expectedFilms = List.of(film1, film2); + + when(filmStorage.getAll()).thenReturn(expectedFilms); + + Collection actualFilms = filmService.getAllFilms(); + + assertNotNull(actualFilms); + assertEquals(2, actualFilms.size()); + assertEquals(expectedFilms, actualFilms); + verify(filmStorage).getAll(); + } + + @Test + public void shouldAddFilmSuccessfully() { + Film inputFilm = new Film(null, "New Film", "Description", LocalDate.now(), 120); + Film savedFilm = new Film(1L, "New Film", "Description", LocalDate.now(), 120); + + when(filmStorage.add(inputFilm)).thenReturn(savedFilm); + doNothing().when(likeStorage).initializeLikesSet(1L); + + Film result = filmService.addFilm(inputFilm); + + assertNotNull(result); + assertEquals(1L, result.getId()); + verify(filmStorage).add(inputFilm); + verify(likeStorage).initializeLikesSet(1L); + } + + @Test + public void shouldUpdateFilmSuccessfully() { + Film filmToUpdate = new Film(1L, "Updated Film", "Updated Desc", LocalDate.now(), 130); + + when(filmStorage.contains(1L)).thenReturn(true); + when(filmStorage.update(filmToUpdate)).thenReturn(filmToUpdate); + + Film result = filmService.updateFilm(filmToUpdate); + + assertNotNull(result); + assertEquals("Updated Film", result.getName()); + verify(filmStorage).update(filmToUpdate); + } + + @Test + public void shouldThrowValidationExceptionWhenUpdateFilmWithNullId() { + Film filmWithoutId = new Film(null, "Film", "Desc", LocalDate.now(), 120); + + ValidationException exception = assertThrows(ValidationException.class, + () -> filmService.updateFilm(filmWithoutId)); + + assertEquals("Id должен быть указан", exception.getMessage()); + verify(filmStorage, never()).update(any()); + } + + @Test + public void shouldThrowNotFoundExceptionWhenUpdateNonExistentFilm() { + Film filmToUpdate = new Film(999L, "Film", "Desc", LocalDate.now(), 120); + + when(filmStorage.contains(999L)).thenReturn(false); + + assertThrows(NotFoundException.class, () -> filmService.updateFilm(filmToUpdate)); + verify(filmStorage, never()).update(any()); + } + + @Test + public void shouldDeleteFilmSuccessfully() { + Long filmId = 1L; + + when(filmStorage.contains(filmId)).thenReturn(true); + + filmService.deleteFilm(filmId); + + verify(filmStorage).remove(filmId); + verify(likeStorage).clearLikesSet(filmId); + } + + @Test + public void shouldThrowNotFoundExceptionWhenDeleteNonExistentFilm() { + Long filmId = 999L; + + when(filmStorage.contains(filmId)).thenReturn(false); + + assertThrows(NotFoundException.class, () -> filmService.deleteFilm(filmId)); + verify(filmStorage, never()).remove(filmId); + verify(likeStorage, never()).clearLikesSet(filmId); + } + + @Test + public void shouldAddLikeSuccessfully() { + Long filmId = 1L; + Long userId = 1L; + Film film = new Film(filmId, "Film", "Desc", LocalDate.now(), 120); + + when(filmStorage.contains(filmId)).thenReturn(true); + when(userStorage.contains(userId)).thenReturn(true); + when(likeStorage.addLike(filmId, userId)).thenReturn(5L); + when(filmStorage.get(filmId)).thenReturn(film); + + Film result = filmService.addLike(filmId, userId); + + assertNotNull(result); + assertEquals(5L, result.getLikes()); + verify(likeStorage).addLike(filmId, userId); + verify(filmStorage, times(2)).get(filmId); + } + + @Test + public void shouldThrowNotFoundExceptionWhenAddLikeToNonExistentFilm() { + Long filmId = 999L; + Long userId = 1L; + + when(filmStorage.contains(filmId)).thenReturn(false); + + assertThrows(NotFoundException.class, () -> filmService.addLike(filmId, userId)); + verify(likeStorage, never()).addLike(anyLong(), anyLong()); + } + + @Test + public void shouldThrowNotFoundExceptionWhenAddLikeFromNonExistentUser() { + Long filmId = 1L; + Long userId = 999L; + + when(filmStorage.contains(filmId)).thenReturn(true); + when(userStorage.contains(userId)).thenReturn(false); + + assertThrows(NotFoundException.class, () -> filmService.addLike(filmId, userId)); + verify(likeStorage, never()).addLike(anyLong(), anyLong()); + } + + @Test + public void shouldRemoveLikeSuccessfully() { + Long filmId = 1L; + Long userId = 1L; + Film film = new Film(filmId, "Film", "Desc", LocalDate.now(), 120); + + when(filmStorage.contains(filmId)).thenReturn(true); + when(userStorage.contains(userId)).thenReturn(true); + when(likeStorage.deleteLike(filmId, userId)).thenReturn(3L); + when(filmStorage.get(filmId)).thenReturn(film); + + Film result = filmService.removeLike(filmId, userId); + + assertNotNull(result); + assertEquals(3L, result.getLikes()); + verify(likeStorage).deleteLike(filmId, userId); + } + + @Test + public void shouldThrowNotFoundExceptionWhenRemoveLikeFromNonExistentFilm() { + Long filmId = 999L; + Long userId = 1L; + + when(filmStorage.contains(filmId)).thenReturn(false); + + assertThrows(NotFoundException.class, () -> filmService.removeLike(filmId, userId)); + verify(likeStorage, never()).deleteLike(anyLong(), anyLong()); + } + + @Test + public void shouldThrowNotFoundExceptionWhenRemoveLikeFromNonExistentUser() { + Long filmId = 1L; + Long userId = 999L; + + when(filmStorage.contains(filmId)).thenReturn(true); + when(userStorage.contains(userId)).thenReturn(false); + + assertThrows(NotFoundException.class, () -> filmService.removeLike(filmId, userId)); + verify(likeStorage, never()).deleteLike(anyLong(), anyLong()); + } + + @Test + public void shouldGetTopFilmsOrderedByLikes() { + Film film1 = new Film(1L, "Film 1", "Desc 1", LocalDate.now(), 120); + film1.setLikes(10L); + Film film2 = new Film(2L, "Film 2", "Desc 2", LocalDate.now(), 150); + film2.setLikes(25L); + Film film3 = new Film(3L, "Film 3", "Desc 3", LocalDate.now(), 130); + film3.setLikes(5L); + + when(filmStorage.getAll()).thenReturn(List.of(film1, film2, film3)); + + Collection topFilms = filmService.getTopFilms(2L); + + assertNotNull(topFilms); + assertEquals(2, topFilms.size()); + + List resultList = new ArrayList<>(topFilms); + assertEquals(25L, resultList.get(0).getLikes()); // film2 + assertEquals(10L, resultList.get(1).getLikes()); // film1 + } + + @Test + public void shouldReturnEmptyListWhenNoFilms() { + when(filmStorage.getAll()).thenReturn(List.of()); + + // when + Collection topFilms = filmService.getTopFilms(10L); + + assertNotNull(topFilms); + assertTrue(topFilms.isEmpty()); + } + + @Test + public void shouldReturnAllFilmsWhenCountIsGreaterThanTotal() { + Film film1 = new Film(1L, "Film 1", "Desc 1", LocalDate.now(), 120); + film1.setLikes(5L); + Film film2 = new Film(2L, "Film 2", "Desc 2", LocalDate.now(), 150); + film2.setLikes(3L); + + when(filmStorage.getAll()).thenReturn(List.of(film1, film2)); + + // when + Collection topFilms = filmService.getTopFilms(5L); + + assertNotNull(topFilms); + assertEquals(2, topFilms.size()); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/service/user/FriendShipServiceTest.java b/src/test/java/ru/yandex/practicum/filmorate/service/user/FriendShipServiceTest.java deleted file mode 100644 index a59f900..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/service/user/FriendShipServiceTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package ru.yandex.practicum.filmorate.service.user; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import ru.yandex.practicum.filmorate.exception.ValidationException; -import ru.yandex.practicum.filmorate.model.User; -import ru.yandex.practicum.filmorate.storage.user.UserStorage; - -import java.time.LocalDate; -import java.util.Collection; -import java.util.List; -import java.util.Objects; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class FriendShipServiceTest { - - @Mock - private UserStorage userStorage; - - private FriendShipService friendShipService; - - @BeforeEach - void setUp() { - friendShipService = new FriendShipService(userStorage); - } - - public Collection addFriend(Long senderId, Long receiverId) { - if (Objects.equals(receiverId, senderId)) { - throw new ValidationException("Сам себя не добавишь - никто не добавит"); - } - - return userStorage.addFriend(senderId, receiverId); - } - - @Test - void shouldThrowValidationExceptionWhenIdsAreEqual() { - assertThrows(ValidationException.class, () -> friendShipService.addFriend(1L, 1L)); - verify(userStorage, never()).addFriend(any(), any()); - } - - @Test - void shouldReturnCollectionWhenIdsAreNotEqual() { - Long senderId = 1L; - Long receiverId = 2L; - List expectedFriends = List.of( - new User(2L, "email", "friend", "Friend", LocalDate.now()) - ); - - when(userStorage.addFriend(senderId, receiverId)).thenReturn(expectedFriends); - - Collection result = friendShipService.addFriend(senderId, receiverId); - - assertThat(result, is(expectedFriends)); - verify(userStorage).addFriend(senderId, receiverId); - } -} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/service/user/UserServiceTest.java b/src/test/java/ru/yandex/practicum/filmorate/service/user/UserServiceTest.java index c8c7cd6..a314020 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/service/user/UserServiceTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/service/user/UserServiceTest.java @@ -5,33 +5,90 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.user.FriendShipStorage; import ru.yandex.practicum.filmorate.storage.user.UserStorage; import java.time.LocalDate; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) -class UserServiceTest { +public class UserServiceTest { @Mock private UserStorage userStorage; @Mock - private FriendShipService friendShipService; + private FriendShipStorage friendShipStorage; - private UserService userService; + private UserServiceInterface userService; @BeforeEach - void setUp() { - userService = new UserService(userStorage, friendShipService); + public void setUp() { + userService = new UserService(userStorage, friendShipStorage); } @Test - void shouldSetNameAsLoginWhenNameIsNull() { + public void shouldGetUser() { + User user = new User(1L, "", "login", null, LocalDate.now()); + when(userStorage.contains(1L)).thenReturn(true); + when(userStorage.get(1L)).thenReturn(user); + User result = userService.getUser(1L); + + assertEquals(user, result); + + verify(userStorage).contains(any()); + verify(userStorage).get(any()); + } + + @Test + public void shouldThrowNotFoundWhenGetUser() { + when(userStorage.contains(1L)).thenReturn(false); + + assertThrows(NotFoundException.class, () -> userService.getUser(1L)); + + verify(userStorage).contains(any()); + verify(userStorage, never()).get(any()); + } + + @Test + public void shouldGetAllUser() { + User user1 = new User(1L, "", "login", null, LocalDate.now()); + User user2 = new User(2L, "", "login", null, LocalDate.now()); + List list = List.of(user1, user2); + + when(userStorage.getAll()).thenReturn(list); + + assertEquals(2, userService.getAllUsers().size()); + + verify(userStorage).getAll(); + } + + @Test + public void shouldAddUser() { + User user1 = new User(null, "email@mail.com", "login", "name", LocalDate.now()); + User returnUser = new User(1L, "email@mail.com", "login", "name", LocalDate.now()); + + when(userStorage.add(any(User.class))).thenReturn(returnUser); + doNothing().when(friendShipStorage).initializeFriendsSet(1L); + + User result = userService.addUser(user1); + + assertEquals(returnUser, result); + verify(userStorage).add(any(User.class)); + verify(friendShipStorage).initializeFriendsSet(1L); + } + + @Test + public void shouldSetNameAsLoginWhenNameIsNull() { // входящая информация User user = new User(1L, "", "login", null, LocalDate.now()); @@ -48,7 +105,7 @@ void shouldSetNameAsLoginWhenNameIsNull() { } @Test - void shouldSetNameAsLoginWhenNameIsEmpty() { + public void shouldSetNameAsLoginWhenNameIsEmpty() { // входящая информация User user = new User(1L, "", "login", "", LocalDate.now()); @@ -65,7 +122,7 @@ void shouldSetNameAsLoginWhenNameIsEmpty() { } @Test - void shouldSetNameAsLoginWhenNameIsBlank() { + public void shouldSetNameAsLoginWhenNameIsBlank() { // входящая информация User user = new User(1L, "", "login", " ", LocalDate.now()); @@ -81,7 +138,7 @@ void shouldSetNameAsLoginWhenNameIsBlank() { } @Test - void shouldDoNothingWhenNameIsPresent() { + public void shouldDoNothingWhenNameIsPresent() { // входящая информация User user = new User(1L, "", "login", "name", LocalDate.now()); @@ -96,4 +153,278 @@ void shouldDoNothingWhenNameIsPresent() { // проверка, что значения равны assertEquals("name", result.getName()); } -} \ No newline at end of file + + @Test + public void shouldUpdateUser() { + User user1 = new User(1L, "email@mail.com", "login", "name", LocalDate.now()); + User returnUser = new User(1L, "email@mail.com", "login", "NAME", LocalDate.now()); + + when(userStorage.contains(any())).thenReturn(true); + when(userStorage.update(any(User.class))).thenReturn(returnUser); + + User result = userService.updateUser(user1); + + assertEquals(returnUser, result); + verify(userStorage).update(any(User.class)); + } + + @Test + public void shouldThrowValidationExceptionWhenUpdateUserWhenIdNull() { + User user1 = new User(null, "email@mail.com", "login", "name", LocalDate.now()); + + assertThrows(ValidationException.class, () -> userService.updateUser(user1)); + verify(userStorage, never()).update(any(User.class)); + } + + @Test + public void shouldThrowNotFoundWhenUpdateUserWhenIdNotFound() { + User user1 = new User(1L, "email@mail.com", "login", "name", LocalDate.now()); + + when(userStorage.contains(any())).thenReturn(false); + + assertThrows(NotFoundException.class, () -> userService.updateUser(user1)); + verify(userStorage, never()).update(any(User.class)); + } + + @Test + public void shouldDeleteUser() { + Long userId = 1L; + User user = new User(userId, "email@mail.com", "login", "name", LocalDate.now()); + + when(userStorage.contains(userId)).thenReturn(true); + when(friendShipStorage.getFriends(userId)).thenReturn(new HashSet<>(Set.of(2L, 3L))); + when(friendShipStorage.getFriends(2L)).thenReturn(new HashSet<>(Set.of(1L, 4L))); + when(friendShipStorage.getFriends(3L)).thenReturn(new HashSet<>(Set.of(1L, 5L))); + + userService.deleteUser(userId); + + verify(userStorage).remove(userId); + verify(friendShipStorage).clearFriendsSet(userId); + verify(friendShipStorage).getFriends(2L); + verify(friendShipStorage).getFriends(3L); + } + + @Test + public void shouldThrowNotFoundWhenDeleteUserWhenIdNotFound() { + when(userStorage.contains(1000L)).thenReturn(false); + + assertThrows(NotFoundException.class, () -> userService.deleteUser(1000L)); + verify(userStorage, never()).remove(1000L); + verify(friendShipStorage, never()).clearFriendsSet(1000L); + } + + @Test + public void shouldRemoveUserFromFriendsFriendsLists() { + Long userId = 1L; + Set friends = Set.of(2L, 3L); + + Set friend2Friends = new HashSet<>(Set.of(1L, 4L, 5L)); + Set friend3Friends = new HashSet<>(Set.of(1L, 6L)); + + when(userStorage.contains(userId)).thenReturn(true); + when(friendShipStorage.getFriends(userId)).thenReturn(friends); + when(friendShipStorage.getFriends(2L)).thenReturn(friend2Friends); + when(friendShipStorage.getFriends(3L)).thenReturn(friend3Friends); + + userService.deleteUser(userId); + + assertFalse(friend2Friends.contains(userId)); + assertFalse(friend3Friends.contains(userId)); + verify(userStorage).remove(userId); + } + + @Test + public void shouldAddFriendSuccessfully() { + Long senderId = 1L; + Long receiverId = 2L; + + when(userStorage.contains(senderId)).thenReturn(true); + when(userStorage.contains(receiverId)).thenReturn(true); + + userService.addFriend(senderId, receiverId); + + verify(friendShipStorage).addFriend(senderId, receiverId); + verify(friendShipStorage).addFriend(receiverId, senderId); + } + + @Test + public void shouldThrowValidationExceptionWhenAddingSelfAsFriend() { + Long userId = 1L; + + ValidationException exception = assertThrows(ValidationException.class, + () -> userService.addFriend(userId, userId)); + + assertEquals("Сам себя не добавишь - никто не добавит", exception.getMessage()); + verify(friendShipStorage, never()).addFriend(anyLong(), anyLong()); + } + + @Test + public void shouldThrowNotFoundExceptionWhenSenderNotFound() { + Long senderId = 999L; + Long receiverId = 2L; + + when(userStorage.contains(senderId)).thenReturn(false); + + assertThrows(NotFoundException.class, () -> userService.addFriend(senderId, receiverId)); + verify(friendShipStorage, never()).addFriend(anyLong(), anyLong()); + } + + @Test + public void shouldThrowNotFoundExceptionWhenReceiverNotFound() { + Long senderId = 1L; + Long receiverId = 999L; + + when(userStorage.contains(senderId)).thenReturn(true); + when(userStorage.contains(receiverId)).thenReturn(false); + + assertThrows(NotFoundException.class, () -> userService.addFriend(senderId, receiverId)); + verify(friendShipStorage, never()).addFriend(anyLong(), anyLong()); + } + + @Test + public void shouldDeleteFriendSuccessfully() { + Long senderId = 1L; + Long receiverId = 2L; + + when(userStorage.contains(senderId)).thenReturn(true); + when(userStorage.contains(receiverId)).thenReturn(true); + + userService.deleteFriend(senderId, receiverId); + + verify(friendShipStorage).deleteFriend(senderId, receiverId); + verify(friendShipStorage).deleteFriend(receiverId, senderId); + } + + @Test + public void shouldThrowNotFoundExceptionWhenDeletingFriendAndSenderNotFound() { + Long senderId = 999L; + Long receiverId = 2L; + + when(userStorage.contains(senderId)).thenReturn(false); + + assertThrows(NotFoundException.class, () -> userService.deleteFriend(senderId, receiverId)); + verify(friendShipStorage, never()).deleteFriend(anyLong(), anyLong()); + } + + @Test + public void shouldThrowNotFoundExceptionWhenDeletingFriendAndReceiverNotFound() { + Long senderId = 1L; + Long receiverId = 999L; + + when(userStorage.contains(senderId)).thenReturn(true); + when(userStorage.contains(receiverId)).thenReturn(false); + + assertThrows(NotFoundException.class, () -> userService.deleteFriend(senderId, receiverId)); + verify(friendShipStorage, never()).deleteFriend(anyLong(), anyLong()); + } + + @Test + public void shouldGetFriendsSuccessfully() { + Long userId = 1L; + Set friendIds = Set.of(2L, 3L); + User friend1 = new User(2L, "friend1@mail.com", "friend1", "Friend One", LocalDate.now()); + User friend2 = new User(3L, "friend2@mail.com", "friend2", "Friend Two", LocalDate.now()); + + when(userStorage.contains(userId)).thenReturn(true); + when(friendShipStorage.getFriends(userId)).thenReturn(friendIds); + when(userStorage.get(2L)).thenReturn(friend1); + when(userStorage.get(3L)).thenReturn(friend2); + + Collection friends = userService.getFriends(userId); + + assertNotNull(friends); + assertEquals(2, friends.size()); + assertTrue(friends.contains(friend1)); + assertTrue(friends.contains(friend2)); + verify(friendShipStorage).getFriends(userId); + verify(userStorage).get(2L); + verify(userStorage).get(3L); + } + + @Test + public void shouldReturnEmptyFriendsList() { + Long userId = 1L; + + when(userStorage.contains(userId)).thenReturn(true); + when(friendShipStorage.getFriends(userId)).thenReturn(Set.of()); + + Collection friends = userService.getFriends(userId); + + assertNotNull(friends); + assertTrue(friends.isEmpty()); + } + + @Test + public void shouldThrowNotFoundExceptionWhenGettingFriendsOfNonExistentUser() { + Long userId = 999L; + + when(userStorage.contains(userId)).thenReturn(false); + + assertThrows(NotFoundException.class, () -> userService.getFriends(userId)); + verify(friendShipStorage, never()).getFriends(anyLong()); + } + + @Test + public void shouldGetCommonFriendsSuccessfully() { + Long user1Id = 1L; + Long user2Id = 2L; + Set user1Friends = Set.of(3L, 4L, 5L); + Set user2Friends = Set.of(4L, 5L, 6L); + + User commonFriend1 = new User(4L, "common1@mail.com", "common1", "Common One", LocalDate.now()); + User commonFriend2 = new User(5L, "common2@mail.com", "common2", "Common Two", LocalDate.now()); + + when(userStorage.contains(user1Id)).thenReturn(true); + when(userStorage.contains(user2Id)).thenReturn(true); + when(friendShipStorage.getFriends(user1Id)).thenReturn(user1Friends); + when(friendShipStorage.getFriends(user2Id)).thenReturn(user2Friends); + when(userStorage.get(4L)).thenReturn(commonFriend1); + when(userStorage.get(5L)).thenReturn(commonFriend2); + + Collection commonFriends = userService.getCommonFriends(user1Id, user2Id); + + assertNotNull(commonFriends); + assertEquals(2, commonFriends.size()); + assertTrue(commonFriends.contains(commonFriend1)); + assertTrue(commonFriends.contains(commonFriend2)); + } + + @Test + public void shouldReturnEmptyCommonFriendsWhenNoCommonFriends() { + Long user1Id = 1L; + Long user2Id = 2L; + Set user1Friends = Set.of(3L, 4L); + Set user2Friends = Set.of(5L, 6L); + + when(userStorage.contains(user1Id)).thenReturn(true); + when(userStorage.contains(user2Id)).thenReturn(true); + when(friendShipStorage.getFriends(user1Id)).thenReturn(user1Friends); + when(friendShipStorage.getFriends(user2Id)).thenReturn(user2Friends); + + Collection commonFriends = userService.getCommonFriends(user1Id, user2Id); + + assertNotNull(commonFriends); + assertTrue(commonFriends.isEmpty()); + } + + @Test + public void shouldThrowNotFoundExceptionWhenFirstUserNotFoundInCommonFriends() { + Long user1Id = 999L; + Long user2Id = 2L; + + when(userStorage.contains(user1Id)).thenReturn(false); + + assertThrows(NotFoundException.class, () -> userService.getCommonFriends(user1Id, user2Id)); + } + + @Test + public void shouldThrowNotFoundExceptionWhenSecondUserNotFoundInCommonFriends() { + Long user1Id = 1L; + Long user2Id = 999L; + + when(userStorage.contains(user1Id)).thenReturn(true); + when(userStorage.contains(user2Id)).thenReturn(false); + + assertThrows(NotFoundException.class, () -> userService.getCommonFriends(user1Id, user2Id)); + } +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorageTest.java index 7be4d80..b7fb5b2 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorageTest.java @@ -2,17 +2,12 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.films.film.InMemoryFilmStorage; import ru.yandex.practicum.filmorate.model.Film; -import java.lang.reflect.Field; -import java.util.List; -import java.util.Map; -import java.util.Set; - import static org.junit.jupiter.api.Assertions.*; -class InMemoryFilmStorageTest { +public class InMemoryFilmStorageTest { private FilmStorage storage; private Film film; @@ -21,156 +16,66 @@ class InMemoryFilmStorageTest { public void setUp() { storage = new InMemoryFilmStorage(); film = new Film(); - } - - private Map> getLikesField() { - - try { - Field field = storage.getClass().getDeclaredField("likes"); - field.setAccessible(true); - return (Map>) field.get(storage); - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } + film.setId(1L); } @Test - public void shouldAddFilmAndCreateLikesSet() { + public void shouldAddObject() { Film user1 = storage.add(film); assertEquals(1L, user1.getId()); assertEquals(film, user1); - assertNotNull(getLikesField().get(1L)); } @Test - public void shouldRemoveFilmAndDeleteLikes() { + public void shouldUpdateObject() { storage.add(film); - Film user1 = storage.remove(1L); + Film user1 = storage.update(film); assertEquals(1L, user1.getId()); assertEquals(film, user1); - assertEquals(0, storage.getAll().size()); - - assertNull(getLikesField().get(1L)); - } - - @Test - public void shouldThrowNotFoundWhenRemoveFilmThatDoesNotExist() { - assertThrows(NotFoundException.class, () -> storage.remove(1L)); - } - - @Test - public void shouldRemoveAllAndClearLikes() { - storage.add(film); - storage.clear(); - - assertEquals(0, storage.getAll().size()); - assertEquals(0, getLikesField().size()); } @Test - public void shouldAddLike() { + public void shouldRemoveObject() { storage.add(film); - storage.addLike(1L, 1L); - storage.addLike(1L, 2L); + Film user1 = storage.remove(1L); - assertEquals(2, film.getLikes()); - assertEquals(2, getLikesField().get(1L).size()); + assertEquals(1L, user1.getId()); + assertEquals(film, user1); + assertEquals(0, storage.getAll().size()); } @Test - public void shouldAddLikeOnlyOnce() { + public void shouldGetObject() { storage.add(film); + Film user1 = storage.get(1L); - storage.addLike(1L, 1L); - storage.addLike(1L, 1L); - - assertEquals(1, film.getLikes()); - assertEquals(1, getLikesField().get(1L).size()); - } - - @Test - public void shouldThrowNotFoundWhenAddLikeToFilmThatDoesNotExist() { - assertThrows(NotFoundException.class, () -> storage.addLike(1L, 1L)); + assertEquals(1L, user1.getId()); + assertEquals(film, user1); } @Test - public void shouldRemoveLike() { - storage.add(film); - - storage.addLike(1L, 1L); - storage.addLike(1L, 2L); - - storage.removeLike(1L, 1L); - - assertEquals(1, film.getLikes()); - assertEquals(1, getLikesField().get(1L).size()); + public void shouldThrowNullPointerExceptionWhenGetObjectThatDoesNotExist() { + assertNull(storage.get(1L)); } @Test - public void shouldRemoveLikeOnlyOnce() { + public void shouldReturnTrueIfContains() { storage.add(film); - - storage.addLike(1L, 1L); - storage.addLike(1L, 2L); - - storage.removeLike(1L, 1L); - storage.removeLike(1L, 1L); - - assertEquals(1, film.getLikes()); - assertEquals(1, getLikesField().get(1L).size()); + assertTrue(storage.contains(1L)); } @Test - public void shouldThrowNotFoundWhenRemoveLikeToFilmThatDoesNotExist() { - assertThrows(NotFoundException.class, () -> storage.removeLike(1L, 1L)); + public void shouldReturnFalseIfNotContains() { + assertFalse(storage.contains(1L)); } @Test - public void shouldReturnTopFilmsInOrder() { + public void shouldReturnSize() { storage.add(film); - storage.add(new Film()); - storage.add(new Film()); - - storage.addLike(3L, 1L); - storage.addLike(3L, 2L); - storage.addLike(3L, 3L); - - storage.addLike(2L, 1L); - storage.addLike(2L, 2L); - - storage.addLike(1L, 1L); - - - List sortedFilms = List.copyOf(storage.getTopFilms(10L)); - assertEquals(3L, sortedFilms.get(0).getId()); - assertEquals(2L, sortedFilms.get(1).getId()); - assertEquals(1L, sortedFilms.get(2).getId()); - } - - @Test - public void shouldReturnOnlyTwoTopFilmsInOrder() { - storage.add(film); - storage.add(new Film()); - storage.add(new Film()); - - storage.addLike(3L, 1L); - storage.addLike(3L, 2L); - storage.addLike(3L, 3L); - - storage.addLike(2L, 1L); - storage.addLike(2L, 2L); - - storage.addLike(1L, 1L); - - - List sortedFilms = List.copyOf(storage.getTopFilms(2L)); - assertEquals(2, sortedFilms.size()); - assertEquals(3L, sortedFilms.get(0).getId()); - assertEquals(2L, sortedFilms.get(1).getId()); + assertEquals(storage.getAll().size(), storage.size()); } -} \ No newline at end of file +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryLikeStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryLikeStorageTest.java new file mode 100644 index 0000000..b51076d --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryLikeStorageTest.java @@ -0,0 +1,99 @@ +package ru.yandex.practicum.filmorate.storage.film; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +public class InMemoryLikeStorageTest { + private LikeStorage storage; + + @BeforeEach + public void setUp() { + storage = new InMemoryLikeStorage(); + } + + private Map> getLikesField() { + + try { + Field field = storage.getClass().getDeclaredField("likes"); + field.setAccessible(true); + return (Map>) field.get(storage); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + @Test + public void shouldCreateLikesSet() { + storage.initializeLikesSet(1L); + assertNotNull(getLikesField().get(1L)); + } + + @Test + public void shouldRemoveSet() { + storage.initializeLikesSet(1L); + storage.clearLikesSet(1L); + assertNull(getLikesField().get(1L)); + } + + @Test + public void shouldAddLike() { + storage.initializeLikesSet(1L); + + storage.addLike(1L, 1L); + storage.addLike(1L, 2L); + + assertEquals(2, getLikesField().get(1L).size()); + } + + @Test + public void shouldAddLikeOnlyOnce() { + storage.initializeLikesSet(1L); + + storage.addLike(1L, 1L); + storage.addLike(1L, 1L); + + assertEquals(1, getLikesField().get(1L).size()); + } + + @Test + public void shouldThrowNullPointerExceptionWhenAddLikeToFilmThatDoesNotExist() { + assertThrows(NullPointerException.class, () -> storage.addLike(1L, 1L)); + } + + @Test + public void shouldRemoveLike() { + storage.initializeLikesSet(1L); + + storage.addLike(1L, 1L); + storage.addLike(1L, 2L); + + storage.deleteLike(1L, 1L); + + assertEquals(1, getLikesField().get(1L).size()); + } + + @Test + public void shouldRemoveLikeOnlyOnce() { + storage.initializeLikesSet(1L); + + storage.addLike(1L, 1L); + storage.addLike(1L, 2L); + + storage.deleteLike(1L, 1L); + storage.deleteLike(1L, 1L); + + assertEquals(1, getLikesField().get(1L).size()); + } + + @Test + public void shouldThrowNullPointerExceptionWhenRemoveLikeToFilmThatDoesNotExist() { + assertThrows(NullPointerException.class, () -> storage.deleteLike(1L, 1L)); + } +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryFriendShipStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryFriendShipStorageTest.java new file mode 100644 index 0000000..058ac5b --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryFriendShipStorageTest.java @@ -0,0 +1,102 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +public class InMemoryFriendShipStorageTest { + + private FriendShipStorage friendShipStorage; + + @BeforeEach + public void setUp() { + friendShipStorage = new InMemoryFriendShipStorage(); + } + + private Map> getFriendShips() { + + try { + Field field = friendShipStorage.getClass().getDeclaredField("friendships"); + field.setAccessible(true); + return (Map>) field.get(friendShipStorage); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + @Test + public void shouldCreateFriendShip() { + friendShipStorage.initializeFriendsSet(1L); + friendShipStorage.initializeFriendsSet(2L); + + friendShipStorage.addFriend(1L, 2L); + friendShipStorage.addFriend(2L, 1L); + + assertTrue(getFriendShips().get(1L).contains(2L)); + assertTrue(getFriendShips().get(2L).contains(1L)); + } + + @Test + public void shouldThrowNullPointerExceptionWhenSenderIsNotFoundCreateFriendShip() { + assertThrows(NullPointerException.class, () -> friendShipStorage.addFriend(1000L, 1L)); + } + + @Test + public void shouldThrowNullPointerExceptionWhenReceiverIsNotFoundCreateFriendShip() { + assertThrows(NullPointerException.class, () -> friendShipStorage.addFriend(1L, 1000L)); + } + + @Test + public void shouldClearUserFriendsAndDeleteUser() { + friendShipStorage.initializeFriendsSet(1L); + friendShipStorage.clearFriendsSet(1L); + assertNull(getFriendShips().get(1L)); + } + + @Test + public void shouldBreakFriendShip() { + friendShipStorage.initializeFriendsSet(1L); + friendShipStorage.initializeFriendsSet(2L); + + friendShipStorage.addFriend(1L, 2L); + friendShipStorage.addFriend(2L, 1L); + + friendShipStorage.deleteFriend(1L, 2L); + friendShipStorage.deleteFriend(2L, 1L); + + assertFalse(getFriendShips().get(1L).contains(2L)); + assertFalse(getFriendShips().get(2L).contains(1L)); + } + + @Test + public void shouldThrowNullPointerExceptionWhenSenderIsNotFoundBreakFriendShip() { + assertThrows(NullPointerException.class, () -> friendShipStorage.deleteFriend(1000L, 1L)); + } + + @Test + public void shouldThrowNullPointerExceptionWhenReceiverIsNotFoundBreakFriendShip() { + assertThrows(NullPointerException.class, () -> friendShipStorage.deleteFriend(1L, 1000L)); + } + + @Test + public void shouldReturnFriends() { + friendShipStorage.initializeFriendsSet(1L); + friendShipStorage.initializeFriendsSet(2L); + + friendShipStorage.addFriend(1L, 2L); + friendShipStorage.addFriend(2L, 1L); + + assertTrue(friendShipStorage.getFriends(2L).contains(1L)); + } + + @Test + public void shouldThrowNullPointerExceptionWhenUserIsNotFoundReturnFriends() { + assertNull(friendShipStorage.getFriends(1000L)); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorageTest.java index 2d236a1..cd48557 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorageTest.java @@ -2,7 +2,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.model.User; import java.lang.reflect.Field; @@ -10,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.*; -class InMemoryUserStorageTest { +public class InMemoryUserStorageTest { private UserStorage storage; private User user; @@ -19,6 +18,7 @@ class InMemoryUserStorageTest { public void setUp() { storage = new InMemoryUserStorage(); user = new User(); + user.setId(1L); } private Map> getFriendShips() { @@ -33,143 +33,64 @@ private Map> getFriendShips() { } } + @Test - public void shouldAddUserAndCreateFriendsMap() { + public void shouldAddObject() { User user1 = storage.add(user); assertEquals(1L, user1.getId()); assertEquals(user, user1); - assertNotNull(getFriendShips().get(1L)); } @Test - public void shouldRemoveUserAndDeleteFriendShips() { + public void shouldUpdateObject() { storage.add(user); - storage.add(new User()); - storage.add(new User()); - - storage.addFriend(1L, 2L); - storage.addFriend(1L, 3L); - - User user1 = storage.remove(1L); + User user1 = storage.update(user); assertEquals(1L, user1.getId()); assertEquals(user, user1); - assertEquals(2, storage.getAll().size()); - - assertFalse(getFriendShips().containsKey(1L)); - assertFalse(getFriendShips().get(2L).containsKey(1L)); - assertFalse(getFriendShips().get(3L).containsKey(1L)); - } - - @Test - public void shouldThrowNotFoundWhenRemoveUserThatDoesNotExist() { - assertThrows(NotFoundException.class, () -> storage.remove(1L)); - } - - @Test - public void shouldRemoveAllAndClearFriends() { - storage.add(user); - storage.clear(); - - assertEquals(0, storage.getAll().size()); - assertEquals(0, getFriendShips().size()); - } - - @Test - public void shouldCreateFriendShip() { - storage.add(user); - storage.add(new User()); - - storage.addFriend(1L, 2L); - - assertTrue(getFriendShips().get(1L).containsKey(2L)); - assertTrue(getFriendShips().get(2L).containsKey(1L)); - } - - @Test - public void shouldThrowNotFoundWhenSenderIsNotFoundCreateFriendShip() { - storage.add(user); - - assertThrows(NotFoundException.class, () -> storage.addFriend(1000L, 1L)); - } - - @Test - public void shouldThrowNotFoundWhenReceiverIsNotFoundCreateFriendShip() { - storage.add(user); - - assertThrows(NotFoundException.class, () -> storage.addFriend(1L, 1000L)); } @Test - public void shouldBreakFriendShip() { + public void shouldRemoveObject() { storage.add(user); - storage.add(new User()); - - storage.addFriend(1L, 2L); - storage.deleteFriend(1L, 2L); - - assertFalse(getFriendShips().get(1L).containsKey(2L)); - assertFalse(getFriendShips().get(2L).containsKey(1L)); - } - @Test - public void shouldThrowNotFoundWhenSenderIsNotFoundBreakFriendShip() { - storage.add(user); - - assertThrows(NotFoundException.class, () -> storage.deleteFriend(1000L, 1L)); - } - - @Test - public void shouldThrowNotFoundWhenReceiverIsNotFoundBreakFriendShip() { - storage.add(user); + User user1 = storage.remove(1L); - assertThrows(NotFoundException.class, () -> storage.deleteFriend(1L, 1000L)); + assertEquals(1L, user1.getId()); + assertEquals(user, user1); + assertEquals(0, storage.getAll().size()); } @Test - public void shouldReturnFriends() { + public void shouldGetObject() { storage.add(user); - storage.add(new User()); - - storage.addFriend(1L, 2L); + User user1 = storage.get(1L); - assertTrue(storage.getFriends(2L).contains(user)); + assertEquals(1L, user1.getId()); + assertEquals(user, user1); } @Test - public void shouldThrowNotFoundWhenUserIsNotFoundReturnFriends() { - assertThrows(NotFoundException.class, () -> storage.getFriends(1000L)); + public void shouldThrowNullPointerExceptionWhenGetObjectThatDoesNotExist() { + assertNull(storage.get(1L)); } @Test - public void shouldReturnCommonFriends() { + public void shouldReturnTrueIfContains() { storage.add(user); - User user2 = storage.add(new User()); - User user3 = storage.add(new User()); - - storage.addFriend(1L, 2L); - storage.addFriend(2L, 3L); - - assertTrue(storage.getCommonFriends(1L, 3L).contains(user2)); - assertFalse(storage.getCommonFriends(1L, 3L).contains(user)); - assertFalse(storage.getCommonFriends(1L, 3L).contains(user3)); - - assertTrue(storage.getCommonFriends(1L, 2L).isEmpty()); - assertTrue(storage.getCommonFriends(2L, 3L).isEmpty()); + assertTrue(storage.contains(1L)); } @Test - public void shouldThrowNotFoundWhenSenderIsNotFoundReturnCommonFriends() { - assertThrows(NotFoundException.class, () -> storage.getCommonFriends(1000L, 1L)); + public void shouldReturnFalseIfNotContains() { + assertFalse(storage.contains(1L)); } @Test - public void shouldThrowNotFoundWhenReceiverIsNotFoundReturnCommonFriends() { + public void shouldReturnSize() { storage.add(user); - - assertThrows(NotFoundException.class, () -> storage.getCommonFriends(1L, 1000L)); + assertEquals(storage.getAll().size(), storage.size()); } - -} \ No newline at end of file +}