diff --git a/pom.xml b/pom.xml index 7e1d9e8..0ffcf4b 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,12 @@ provided + + org.zalando + logbook-spring-boot-starter + 3.7.2 + + org.junit.jupiter junit-jupiter 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..e490cb5 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.FilmServiceInterface; 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 FilmServiceInterface filmService; /** - * Private constructor to initialize ID generator with starting ID = 1. + * Constructor for dependency injection */ - private FilmController() { - idGenerator.set(1); + public FilmController(FilmServiceInterface 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); } } 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..975e081 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.UserServiceInterface; 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 UserServiceInterface userService; /** - * Private constructor to initialize ID generator with starting ID = 1. + * Constructor for dependency injection */ - private UserController() { - idGenerator.set(1); + public UserController(UserServiceInterface 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,24 +60,70 @@ 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) { - if (newUser.getId() == null) { - 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. + * @throws NotFoundException if user is not found + * @throws ValidationException if id equals friendId + */ + @PutMapping("/{id}/friends/{friendId}") + public void addFriend(@PathVariable @Positive Long id, + @PathVariable @Positive Long friendId) { + 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); } } 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); } } 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..6fe8c60 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -1,18 +1,19 @@ 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 ru.yandex.practicum.filmorate.validation.ValidReleaseDate; import java.time.LocalDate; + /** - * User. - *

DTO to represent user + * Film. + *

DTO to represent film * *

Properties: *

*/ + @Data @NoArgsConstructor -@AllArgsConstructor public class Film { + public Film(Long id, String name, String description, LocalDate releaseDate, Integer duration) { + this.id = id; + this.name = name; + this.description = description; + this.releaseDate = releaseDate; + this.duration = duration; + } + private Long id; @NotBlank(message = "Название не может быть пустым") @@ -41,4 +51,7 @@ public class Film { @Positive private Integer duration; + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private Long likes = 0L; } 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; } 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..b53d904 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java @@ -0,0 +1,169 @@ +package ru.yandex.practicum.filmorate.service.film; + +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. + *

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

+ * + * @see Film + * @see FilmStorage + */ +@Service +public class FilmService implements FilmServiceInterface { + + private final FilmStorage filmStorage; + private final UserStorage userStorage; + private final LikeStorage likeStorage; + + /** + * Constructor for dependency injection + */ + public FilmService(FilmStorage filmStorage, UserStorage userStorage, LikeStorage likeStorage) { + this.filmStorage = filmStorage; + this.userStorage = userStorage; + this.likeStorage = likeStorage; + } + + /** + * Retrieves specific film from the storage. + * + * @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); + } + + /** + * Retrieves all filmsStorage from the storage. + * + * @return Collection of all filmsStorage. + */ + @Override + public Collection getAllFilms() { + return filmStorage.getAll(); + } + + /** + * Adds film to the storage. + * + * @param film Film to add. + * @return created Film. + */ + @Override + public Film addFilm(Film film) { + Film returnFilm = filmStorage.add(film); + likeStorage.initializeLikesSet(returnFilm.getId()); + return returnFilm; + } + + /** + * Updates film in the storage. + * + * @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); + } + + /** + * 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); + } + + /** + * 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. + * @throws NotFoundException if film or user not found + */ + @Override + public Film addLike(Long filmId, Long 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); + } + + /** + * 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. + * @throws NotFoundException if film or user not found + */ + @Override + public Film removeLike(Long filmId, Long 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); + } + + /** + * Return top ranked films. + * + * @param count limit to collection. + * @return collection of Films. + */ + @Override + public Collection getTopFilms(Long 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/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/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java new file mode 100644 index 0000000..eff4749 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java @@ -0,0 +1,194 @@ +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. + *

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

+ * + * @see User + * @see UserStorage + */ +@Service +public class UserService implements UserServiceInterface { + + private final UserStorage userStorage; + private final FriendShipStorage friendShipStorage; + + /** + * Constructor for dependency injection + */ + public UserService(UserStorage userStorage, FriendShipStorage friendShipStorage) { + this.userStorage = userStorage; + 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 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(); + } + + /** + * 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()); + } + + User returnUser = userStorage.add(user); + friendShipStorage.initializeFriendsSet(returnUser.getId()); + + return returnUser; + } + + /** + * Updates user in the storage. + * + * @param user User to replace. User`s id must be in the system. + * @return updated User. + * @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); + } + + /** + * 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 NotFoundException if user is not found + * @throws ValidationException if senderId equals 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); + } + + /** + * 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 NotFoundException if user is not found + */ + @Override + public void deleteFriend(Long senderId, Long 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 NotFoundException if user is not found + */ + @Override + public Collection getFriends(Long id) { + throwIfNotFound(id); + + return friendShipStorage.getFriends(id).stream() + .map(userStorage::get) + .toList(); + } + + /** + * 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 NotFoundException if user is not found + */ + @Override + public Collection getCommonFriends(Long id, Long 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 + " не найден"); + } + } +} 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); +} 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..0d5f7d7 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/BasicStorage.java @@ -0,0 +1,21 @@ +package ru.yandex.practicum.filmorate.storage; + +import java.util.Collection; + +public interface BasicStorage { + + T add(T t); + + T update(T t); + + T remove(Long id); + + T get(Long id); + + Collection getAll(); + + boolean contains(Long id); + + int size(); + +} 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..6e55005 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.storage.film; + +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.storage.BasicStorage; + +public interface FilmStorage extends BasicStorage { +} 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..82b593c --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java @@ -0,0 +1,71 @@ +package ru.yandex.practicum.filmorate.films.film; + +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Film; +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 films. + *

+ * 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 implements FilmStorage { + + private final Map films; + private final AtomicLong idGenerator; + + public InMemoryFilmStorage() { + this.films = new ConcurrentHashMap<>(); + this.idGenerator = new AtomicLong(1); + } + + @Override + public Film add(Film film) { + film.setId(idGenerator.getAndIncrement()); + films.put(film.getId(), film); + + return films.get(film.getId()); + } + + @Override + public Film update(Film newFilm) { + films.put(newFilm.getId(), newFilm); + + return films.get(newFilm.getId()); + } + + @Override + public Film remove(Long id) { + return films.remove(id); + } + + @Override + public Film get(Long id) { + return films.get(id); + } + + @Override + public Collection getAll() { + return List.copyOf(films.values()); + } + + @Override + public boolean contains(Long id) { + return films.containsKey(id); + } + + @Override + public int size() { + return films.size(); + } +} 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); +} 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); + } +} 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..4b26fae --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java @@ -0,0 +1,72 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.User; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +/** + * In-memory implementation of user users. + *

+ * 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 implements UserStorage { + + private final Map users; + private final AtomicLong idGenerator; + + public InMemoryUserStorage() { + this.users = new ConcurrentHashMap<>(); + this.idGenerator = new AtomicLong(1); + } + + @Override + public User add(User user) { + user.setId(idGenerator.getAndIncrement()); + users.put(user.getId(), user); + + return users.get(user.getId()); + } + + @Override + public User update(User newUser) { + users.put(newUser.getId(), newUser); + + return users.get(newUser.getId()); + } + + @Override + public User remove(Long id) { + return users.remove(id); + } + + @Override + public User get(Long id) { + return users.get(id); + } + + @Override + public Collection getAll() { + return List.copyOf(users.values()); + } + + @Override + public boolean contains(Long id) { + return users.containsKey(id); + } + + @Override + public int size() { + return users.size(); + } +} 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..ae4c687 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.BasicStorage; + +public interface UserStorage extends BasicStorage { +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/validation/BirthdayValidator.java b/src/main/java/ru/yandex/practicum/filmorate/validation/BirthdayValidator.java deleted file mode 100644 index 0dab21a..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/validation/BirthdayValidator.java +++ /dev/null @@ -1,28 +0,0 @@ -package ru.yandex.practicum.filmorate.validation; - -import jakarta.validation.ConstraintValidator; -import jakarta.validation.ConstraintValidatorContext; - -import java.time.LocalDate; - -/** - * Custom validator for {@link ValidBirthday} - */ -public class BirthdayValidator implements ConstraintValidator { - - /** - * Validates that the birthday is not in the future. - * - * @param localDate the date to validate - * @param constraintValidatorContext validation context - * @return true if date is null or in past or present, false otherwise - */ - @Override - public boolean isValid(LocalDate localDate, ConstraintValidatorContext constraintValidatorContext) { - if (localDate == null) { - return true; - } - - return !localDate.isAfter(LocalDate.now()); - } -} diff --git a/src/main/java/ru/yandex/practicum/filmorate/validation/ValidBirthday.java b/src/main/java/ru/yandex/practicum/filmorate/validation/ValidBirthday.java deleted file mode 100644 index 836e580..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/validation/ValidBirthday.java +++ /dev/null @@ -1,26 +0,0 @@ -package ru.yandex.practicum.filmorate.validation; - -import jakarta.validation.Constraint; -import jakarta.validation.Payload; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Validates that a birthdate is not in the future. - * - *

Applied to {@code LocalDate} fields to ensure the date represents a past or present date. - * Used primarily for user birthdate validation.

- */ -@Target({ElementType.FIELD}) -@Retention(RetentionPolicy.RUNTIME) -@Constraint(validatedBy = BirthdayValidator.class) -public @interface ValidBirthday { - String message() default "Дата рождения не может быть в будущем"; - - Class[] groups() default {}; - - Class[] payload() default {}; -} \ No newline at end of file diff --git a/application.properties b/src/main/resources/application-dev.properties similarity index 68% rename from application.properties rename to src/main/resources/application-dev.properties index f93a4cf..b50b2c2 100644 --- a/application.properties +++ b/src/main/resources/application-dev.properties @@ -1,9 +1,12 @@ spring.application.name=Filmorate -server.port=8080 +server.port=8081 logging.file.path=./logs/ logging.logback.rollingpolicy.file-name-pattern=./logs/spring.%d{yyyy-MM-dd}.%i.log logging.logback.rollingpolicy.max-file-size=10MB logging.logback.rollingpolicy.total-size-cap=50MB logging.logback.rollingpolicy.clean-history-on-start=true -spring.output.ansi.enabled=ALWAYS \ No newline at end of file +spring.output.ansi.enabled=ALWAYS + +logging.level.ru.yandex.practicum.filmorate=INFO +logging.level.org.zalando.logbook=TRACE \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..45001af --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,12 @@ +spring.application.name=Filmorate +server.port=8080 + +logging.file.path=./logs/ +logging.logback.rollingpolicy.file-name-pattern=./logs/spring.%d{yyyy-MM-dd}.%i.log +logging.logback.rollingpolicy.max-file-size=10MB +logging.logback.rollingpolicy.total-size-cap=40MB +logging.logback.rollingpolicy.clean-history-on-start=true +spring.output.ansi.enabled=ALWAYS + +logging.level.ru.yandex.practicum.filmorate=WARN +logging.level.org.zalando.logbook=WARN \ No newline at end of file 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..7884b1a 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,20 @@ 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.exception.ValidationException; 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 +22,406 @@ @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); + + 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()); + + 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()); + } - @BeforeEach - public void getNewText() throws JsonProcessingException { - User user = new User(2L, - "valid@mail.com", - "newlogin", - "same George", - LocalDate.of(2000, 1, 1) - ); + @Test + public void shouldReturn400WhenFutureBirthday() throws Exception { + String invalidJson = "{\"email\":\"test@mail.ru\",\"login\":\"login\",\"name\":\"Name\",\"birthday\":\"2030-01-01\"}"; - json = objectMapper.writeValueAsString(user); + 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 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, times(1)).updateUser(any()); } @Test public void shouldNotUpdateUserWhenNotFound() 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 { + mockMvc.perform(put("/users/3/friends/2")) + .andExpect(status().isOk()); - mockMvc.perform(put("/users") - .contentType(MediaType.APPLICATION_JSON) - .content(json)) + verify(userService, times(1)).addFriend(any(), any()); + } + + @Test + public void shouldReturnNotFoundWhenSenderNotFound() throws Exception { + + doThrow(new NotFoundException("Пользователь с id = 1000 не найден")) + .when(userService).addFriend(1000L, 1L); + + mockMvc.perform(put("/users/1000/friends/1")) + .andExpect(status().isNotFound()); + + verify(userService, times(1)).addFriend(1000L, 1L); + } + + @Test + public void shouldReturnNotFoundWhenReceiverNotFound() throws Exception { + + doThrow(new NotFoundException("Пользователь с id = 1000 не найден")) + .when(userService).addFriend(1L, 1000L); + + + 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 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/UserServiceTest.java b/src/test/java/ru/yandex/practicum/filmorate/service/user/UserServiceTest.java new file mode 100644 index 0000000..a314020 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/service/user/UserServiceTest.java @@ -0,0 +1,430 @@ +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.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.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class UserServiceTest { + + @Mock + private UserStorage userStorage; + + @Mock + private FriendShipStorage friendShipStorage; + + private UserServiceInterface userService; + + @BeforeEach + public void setUp() { + userService = new UserService(userStorage, friendShipStorage); + } + + @Test + 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()); + + // настраиваем поведение мока + when(userStorage.add(user)).thenReturn(user); + + User result = userService.addUser(user); + + // проверки, что метод вызвался + verify(userStorage).add(user); + + // проверка, что значения равны + assertEquals("login", result.getName()); + } + + @Test + public 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 + public 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 + public 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()); + } + + @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 new file mode 100644 index 0000000..b7fb5b2 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorageTest.java @@ -0,0 +1,81 @@ +package ru.yandex.practicum.filmorate.storage.film; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import ru.yandex.practicum.filmorate.films.film.InMemoryFilmStorage; +import ru.yandex.practicum.filmorate.model.Film; + +import static org.junit.jupiter.api.Assertions.*; + +public class InMemoryFilmStorageTest { + + private FilmStorage storage; + private Film film; + + @BeforeEach + public void setUp() { + storage = new InMemoryFilmStorage(); + film = new Film(); + film.setId(1L); + } + + @Test + public void shouldAddObject() { + Film user1 = storage.add(film); + + assertEquals(1L, user1.getId()); + assertEquals(film, user1); + } + + @Test + public void shouldUpdateObject() { + storage.add(film); + + Film user1 = storage.update(film); + + assertEquals(1L, user1.getId()); + assertEquals(film, user1); + } + + @Test + public void shouldRemoveObject() { + storage.add(film); + + Film user1 = storage.remove(1L); + + assertEquals(1L, user1.getId()); + assertEquals(film, user1); + assertEquals(0, storage.getAll().size()); + } + + @Test + public void shouldGetObject() { + storage.add(film); + Film user1 = storage.get(1L); + + assertEquals(1L, user1.getId()); + assertEquals(film, user1); + } + + @Test + public void shouldThrowNullPointerExceptionWhenGetObjectThatDoesNotExist() { + assertNull(storage.get(1L)); + } + + @Test + public void shouldReturnTrueIfContains() { + storage.add(film); + assertTrue(storage.contains(1L)); + } + + @Test + public void shouldReturnFalseIfNotContains() { + assertFalse(storage.contains(1L)); + } + + @Test + public void shouldReturnSize() { + storage.add(film); + assertEquals(storage.getAll().size(), storage.size()); + } +} 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 new file mode 100644 index 0000000..cd48557 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorageTest.java @@ -0,0 +1,96 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import ru.yandex.practicum.filmorate.model.User; + +import java.lang.reflect.Field; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class InMemoryUserStorageTest { + + private UserStorage storage; + private User user; + + @BeforeEach + public void setUp() { + storage = new InMemoryUserStorage(); + user = new User(); + user.setId(1L); + } + + 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 shouldAddObject() { + User user1 = storage.add(user); + + assertEquals(1L, user1.getId()); + assertEquals(user, user1); + } + + @Test + public void shouldUpdateObject() { + storage.add(user); + + User user1 = storage.update(user); + + assertEquals(1L, user1.getId()); + assertEquals(user, user1); + } + + @Test + public void shouldRemoveObject() { + storage.add(user); + + User user1 = storage.remove(1L); + + assertEquals(1L, user1.getId()); + assertEquals(user, user1); + assertEquals(0, storage.getAll().size()); + } + + @Test + public void shouldGetObject() { + storage.add(user); + User user1 = storage.get(1L); + + assertEquals(1L, user1.getId()); + assertEquals(user, user1); + } + + @Test + public void shouldThrowNullPointerExceptionWhenGetObjectThatDoesNotExist() { + assertNull(storage.get(1L)); + } + + @Test + public void shouldReturnTrueIfContains() { + storage.add(user); + assertTrue(storage.contains(1L)); + } + + @Test + public void shouldReturnFalseIfNotContains() { + assertFalse(storage.contains(1L)); + } + + @Test + public void shouldReturnSize() { + storage.add(user); + assertEquals(storage.getAll().size(), storage.size()); + } +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/validation/BirthdayValidatorTest.java b/src/test/java/ru/yandex/practicum/filmorate/validation/BirthdayValidatorTest.java deleted file mode 100644 index 6dd425f..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/validation/BirthdayValidatorTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package ru.yandex.practicum.filmorate.validation; - -import org.junit.jupiter.api.Test; - -import java.time.LocalDate; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class BirthdayValidatorTest { - private final BirthdayValidator validator = new BirthdayValidator(); - - @Test - public void shouldReturnTrueForValidDate() { - LocalDate birthday = LocalDate.of(1595, 1, 28); - assertTrue(validator.isValid(birthday, null)); - } - - @Test - public void shouldReturnTrueForToday() { - LocalDate future = LocalDate.now(); - assertTrue(validator.isValid(future, null)); - } - - @Test - public void shouldReturnFalseForFutureDate() { - LocalDate future = LocalDate.now().plusDays(1); - assertFalse(validator.isValid(future, null)); - } - - @Test - public void shouldReturnTrueForNull() { - assertTrue(validator.isValid(null, null)); - } -} \ No newline at end of file