diff --git a/README.md b/README.md index 289eae4..f4cb016 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,40 @@ Raw OpenAPI specification (JSON): http://localhost:8080/v3/api-docs ``` +### Available endpoints + +#### Generate and persist random users + +```http +GET /random-users?count=500 +``` + +#### Update an existing user + +```http +PUT /random-users/{id} +Content-Type: application/json +``` + +Example body: + +```json +{ + "gender": "female", + "firstname": "Albert", + "lastname": "Bing", + "civility": "Mrs", + "email": "albert.bing@example.com", + "phone": "123456789", + "picture": "pic.jpg", + "nat": "FR" +} +``` + +Responses: +- `200` if the user was updated successfully +- `404` if the user id does not exist + --- ## 🔍 Monitoring (Actuator) @@ -281,7 +315,7 @@ This project consumes the public **Random User Generator** API: - [x] [Add PostgreSQL database with docker](https://github.com/XPEHO/spring_boot_java_random_user/issues/6) - [X] [Add this endpoint get /user/random](https://github.com/XPEHO/spring_boot_java_random_user/issues/5) - [ ] [Add this endpoint get /user/{id}](https://github.com/XPEHO/spring_boot_java_random_user/issues/8) -- [ ] [Add this endpoint put /user/{id}](https://github.com/XPEHO/spring_boot_java_random_user/issues/9) +- [X] [Add this endpoint put /user/{id}](https://github.com/XPEHO/spring_boot_java_random_user/issues/9) - [ ] [Add this endpoint delete /user/{id}](https://github.com/XPEHO/spring_boot_java_random_user/issues/10) - [ ] [Add this endpoint post /user](https://github.com/XPEHO/spring_boot_java_random_user/issues/11) diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/data/services/UserServiceImpl.java b/src/main/java/com/xpeho/spring_boot_java_random_user/data/services/UserServiceImpl.java index 3dae4e3..adc4723 100644 --- a/src/main/java/com/xpeho/spring_boot_java_random_user/data/services/UserServiceImpl.java +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/services/UserServiceImpl.java @@ -8,6 +8,7 @@ import org.springframework.stereotype.Service; import java.util.List; +import java.util.Optional; import java.util.stream.StreamSupport; @Service @@ -29,4 +30,16 @@ public List saveAll(List users) { .map(userConverter::toDomain) .toList(); } + + @Override + public Optional getById(long id) { + return userRepository.findById(id) + .map(userConverter::toDomain); + } + + @Override + public UserEntity save(UserEntity user) { + User savedUser = userRepository.save(userConverter.toDao(user)); + return userConverter.toDomain(savedUser); + } } diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/domain/entities/UserRequest.java b/src/main/java/com/xpeho/spring_boot_java_random_user/domain/entities/UserRequest.java new file mode 100644 index 0000000..69ffa3a --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/domain/entities/UserRequest.java @@ -0,0 +1,12 @@ +package com.xpeho.spring_boot_java_random_user.domain.entities; + +public record UserRequest( + String gender, + String firstname, + String lastname, + String civility, + String email, + String phone, + String picture, + String nat +) {} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/domain/exceptions/UserNotFoundException.java b/src/main/java/com/xpeho/spring_boot_java_random_user/domain/exceptions/UserNotFoundException.java new file mode 100644 index 0000000..e53e627 --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/domain/exceptions/UserNotFoundException.java @@ -0,0 +1,7 @@ +package com.xpeho.spring_boot_java_random_user.domain.exceptions; + +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException(long id) { + super("User not found with id: " + id); + } +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/domain/services/UserService.java b/src/main/java/com/xpeho/spring_boot_java_random_user/domain/services/UserService.java index 2462da7..f09a8af 100644 --- a/src/main/java/com/xpeho/spring_boot_java_random_user/domain/services/UserService.java +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/domain/services/UserService.java @@ -2,7 +2,12 @@ import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity; import java.util.List; +import java.util.Optional; public interface UserService { List saveAll(List users); + + Optional getById(long id); + + UserEntity save(UserEntity user); } diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/domain/usecases/UpdateRandomUserUseCase.java b/src/main/java/com/xpeho/spring_boot_java_random_user/domain/usecases/UpdateRandomUserUseCase.java new file mode 100644 index 0000000..26a73f5 --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/domain/usecases/UpdateRandomUserUseCase.java @@ -0,0 +1,35 @@ +package com.xpeho.spring_boot_java_random_user.domain.usecases; + +import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity; +import com.xpeho.spring_boot_java_random_user.domain.entities.UserRequest; +import com.xpeho.spring_boot_java_random_user.domain.exceptions.UserNotFoundException; +import com.xpeho.spring_boot_java_random_user.domain.services.UserService; +import org.springframework.stereotype.Service; + +@Service +public class UpdateRandomUserUseCase { + private final UserService userService; + + public UpdateRandomUserUseCase(UserService userService) { + this.userService = userService; + } + + public UserEntity execute(int id, UserRequest user) { + UserEntity existingUser = userService.getById(id) + .orElseThrow(() -> new UserNotFoundException(id)); + + UserEntity updatedUser = new UserEntity( + existingUser.id(), + user.gender(), + user.firstname(), + user.lastname(), + user.civility(), + user.email(), + user.phone(), + user.picture(), + user.nat() + ); + + return userService.save(updatedUser); + } +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/presentation/controllers/UserController.java b/src/main/java/com/xpeho/spring_boot_java_random_user/presentation/controllers/UserController.java index 530f82d..622f434 100644 --- a/src/main/java/com/xpeho/spring_boot_java_random_user/presentation/controllers/UserController.java +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/presentation/controllers/UserController.java @@ -1,11 +1,15 @@ package com.xpeho.spring_boot_java_random_user.presentation.controllers; +import com.xpeho.spring_boot_java_random_user.domain.entities.UserRequest; import org.springframework.http.ResponseEntity; import java.util.List; import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -37,4 +41,22 @@ ResponseEntity> getRandomUsers( @Max(5000) int count ); + @PutMapping("/{id}") + @Operation( + summary = "Modify a random user", + description = "Giving a random user id, modify the content of the user", + parameters = { + @Parameter(name = "id", description = "id of the requested user"), + @Parameter(name= "UserEntity", description = "changeable parameters") + } + ) + @ApiResponse(responseCode = "200", description = "User successfully modified and saved") + @ApiResponse(responseCode = "404", description = "The requested user does not exist") + @ApiResponse(responseCode = "500", description = "Internal server error") + ResponseEntity updateRandomUser( + @PathVariable + int id, + @RequestBody + UserRequest user + ); } diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/presentation/handlers/UserHandler.java b/src/main/java/com/xpeho/spring_boot_java_random_user/presentation/handlers/UserHandler.java index 50f860a..49a6203 100644 --- a/src/main/java/com/xpeho/spring_boot_java_random_user/presentation/handlers/UserHandler.java +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/presentation/handlers/UserHandler.java @@ -1,7 +1,10 @@ package com.xpeho.spring_boot_java_random_user.presentation.handlers; import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity; +import com.xpeho.spring_boot_java_random_user.domain.entities.UserRequest; +import com.xpeho.spring_boot_java_random_user.domain.exceptions.UserNotFoundException; import com.xpeho.spring_boot_java_random_user.domain.usecases.FetchAndSaveRandomUsersUseCase; +import com.xpeho.spring_boot_java_random_user.domain.usecases.UpdateRandomUserUseCase; import com.xpeho.spring_boot_java_random_user.presentation.controllers.UserController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,9 +23,14 @@ public class UserHandler implements UserController { private static final Logger logger = LoggerFactory.getLogger(UserHandler.class); private final FetchAndSaveRandomUsersUseCase fetchAndSaveRandomUsersUseCase; + private final UpdateRandomUserUseCase updateRandomUserUseCase; - public UserHandler(FetchAndSaveRandomUsersUseCase fetchAndSaveRandomUsersUseCase) { + public UserHandler( + FetchAndSaveRandomUsersUseCase fetchAndSaveRandomUsersUseCase, + UpdateRandomUserUseCase updateRandomUserUseCase + ) { this.fetchAndSaveRandomUsersUseCase = fetchAndSaveRandomUsersUseCase; + this.updateRandomUserUseCase = updateRandomUserUseCase; } @Override @@ -36,4 +44,15 @@ public ResponseEntity> getRandomUsers(int count) { .body(emptyList()); } } -} \ No newline at end of file + @Override + public ResponseEntity updateRandomUser(int id, UserRequest user) { + try { + UserEntity savedUser = updateRandomUserUseCase.execute(id, user); + return ResponseEntity.ok(savedUser); + } catch (UserNotFoundException e) { + logger.warn("warning: the requested user does not exist : {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } + } + +} diff --git a/src/test/java/com/xpeho/spring_boot_java_random_user/data/services/UserServiceImplTest.java b/src/test/java/com/xpeho/spring_boot_java_random_user/data/services/UserServiceImplTest.java new file mode 100644 index 0000000..2b86838 --- /dev/null +++ b/src/test/java/com/xpeho/spring_boot_java_random_user/data/services/UserServiceImplTest.java @@ -0,0 +1,88 @@ +package com.xpeho.spring_boot_java_random_user.data.services; + +import com.xpeho.spring_boot_java_random_user.data.converters.UserConverter; +import com.xpeho.spring_boot_java_random_user.data.models.db.User; +import com.xpeho.spring_boot_java_random_user.data.sources.database.UserRepository; +import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class UserServiceImplTest { + private UserRepository userRepository; + private UserConverter userConverter; + private UserServiceImpl userService; + + @BeforeEach + void setUp() { + userRepository = mock(UserRepository.class); + userConverter = mock(UserConverter.class); + userService = new UserServiceImpl(userRepository, userConverter); + } + + @Test + @DisplayName("Should return mapped user when id exists") + void shouldReturnMappedUserWhenIdExists() { + User dao = new User(); + dao.setId(1L); + dao.setFirstname("John"); + + UserEntity expected = new UserEntity(1L, "male", "John", "Doe", "Mr", "john@doe.com", "1234", "pic.jpg", "FR"); + + when(userRepository.findById(1L)).thenReturn(Optional.of(dao)); + when(userConverter.toDomain(dao)).thenReturn(expected); + + Optional result = userService.getById(1L); + + assertTrue(result.isPresent()); + assertEquals(expected, result.get()); + verify(userRepository).findById(1L); + verify(userConverter).toDomain(dao); + } + + @Test + @DisplayName("Should return empty optional when id does not exist") + void shouldReturnEmptyOptionalWhenIdDoesNotExist() { + when(userRepository.findById(2L)).thenReturn(Optional.empty()); + + Optional result = userService.getById(2L); + + assertTrue(result.isEmpty()); + verify(userRepository).findById(2L); + } + + @Test + @DisplayName("Should save mapped user and return mapped domain entity") + void shouldSaveMappedUserAndReturnMappedDomainEntity() { + UserEntity input = new UserEntity(3L, "female", "Alice", "Smith", "Mrs", "alice@smith.com", "5678", "new-pic.jpg", "US"); + + User daoToSave = new User(); + daoToSave.setId(3L); + daoToSave.setFirstname("Alice"); + + User savedDao = new User(); + savedDao.setId(3L); + savedDao.setFirstname("Alice"); + + UserEntity expected = new UserEntity(3L, "female", "Alice", "Smith", "Mrs", "alice@smith.com", "5678", "new-pic.jpg", "US"); + + when(userConverter.toDao(input)).thenReturn(daoToSave); + when(userRepository.save(daoToSave)).thenReturn(savedDao); + when(userConverter.toDomain(savedDao)).thenReturn(expected); + + UserEntity result = userService.save(input); + + assertEquals(expected, result); + verify(userConverter).toDao(input); + verify(userRepository).save(daoToSave); + verify(userConverter).toDomain(savedDao); + } +} diff --git a/src/test/java/com/xpeho/spring_boot_java_random_user/domain/usecases/UpdateRandomUserUseCaseTest.java b/src/test/java/com/xpeho/spring_boot_java_random_user/domain/usecases/UpdateRandomUserUseCaseTest.java new file mode 100644 index 0000000..8aee528 --- /dev/null +++ b/src/test/java/com/xpeho/spring_boot_java_random_user/domain/usecases/UpdateRandomUserUseCaseTest.java @@ -0,0 +1,76 @@ +package com.xpeho.spring_boot_java_random_user.domain.usecases; + +import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity; +import com.xpeho.spring_boot_java_random_user.domain.entities.UserRequest; +import com.xpeho.spring_boot_java_random_user.domain.exceptions.UserNotFoundException; +import com.xpeho.spring_boot_java_random_user.domain.services.UserService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class UpdateRandomUserUseCaseTest { + private UserService userService; + private UpdateRandomUserUseCase useCase; + + @BeforeEach + void setUp() { + userService = mock(UserService.class); + useCase = new UpdateRandomUserUseCase(userService); + } + + @Test + @DisplayName("Should update an existing user and preserve its id") + void shouldUpdateExistingUserAndPreserveItsId() { + UserEntity existingUser = new UserEntity( + 42L, "male", "John", "Doe", "Mr", "john@doe.com", "1234", "pic.jpg", "FR" + ); + UserRequest request = new UserRequest( + "female", "Alice", "Smith", "Mrs", "alice@smith.com", "5678", "new-pic.jpg", "US" + ); + UserEntity savedUser = new UserEntity( + 42L, "female", "Alice", "Smith", "Mrs", "alice@smith.com", "5678", "new-pic.jpg", "US" + ); + + when(userService.getById(42)).thenReturn(Optional.of(existingUser)); + when(userService.save(new UserEntity( + 42L, "female", "Alice", "Smith", "Mrs", "alice@smith.com", "5678", "new-pic.jpg", "US" + ))).thenReturn(savedUser); + + UserEntity result = useCase.execute(42, request); + + assertEquals(savedUser, result); + verify(userService).getById(42); + verify(userService).save(new UserEntity( + 42L, "female", "Alice", "Smith", "Mrs", "alice@smith.com", "5678", "new-pic.jpg", "US" + )); + } + + @Test + @DisplayName("Should throw when updating a user that does not exist") + void shouldThrowWhenUserDoesNotExist() { + UserRequest request = new UserRequest( + "female", "Alice", "Smith", "Mrs", "alice@smith.com", "5678", "new-pic.jpg", "US" + ); + + when(userService.getById(99)).thenReturn(Optional.empty()); + + UserNotFoundException exception = assertThrows( + UserNotFoundException.class, + () -> useCase.execute(99, request) + ); + + assertEquals("User not found with id: 99", exception.getMessage()); + verify(userService).getById(99); + verify(userService, never()).save(any(UserEntity.class)); + } +}