diff --git a/.gitignore b/.gitignore index 83d80fc..3532613 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +application-local.properties +.env HELP.md target/ .mvn/wrapper/maven-wrapper.jar diff --git a/README.md b/README.md index 7e0284b..289eae4 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,54 @@ cp src/test/resources/application-test.properties.template src/test/resources/ap ``` > ⚠️ This file is also git-ignored. +But for credentials and local overrides, use: + +``` +src/main/resources/application-local.properties +``` + +### Example for application.properties + +```properties +spring.application.name=spring_boot_java_random_user +springdoc.swagger-ui.path=/api +spring.datasource.url=jdbc:postgresql://localhost:5432/ +spring.datasource.driver-class-name=org.postgresql.Driver +``` + +### Local credentials & security + +To avoid exposing database credentials in source code, create a `.env` file at the project root. + +Then, in `src/main/resources/application-local.properties`: + +```properties +spring.datasource.username=${DB_USER} +spring.datasource.password=${DB_PASSWORD} +``` + +Both `.env` and `application-local.properties` are in `.gitignore` (already set). + +To activate the local profile in IntelliJ or VS Code, add this environment variable to your run configuration: + +``` +SPRING_PROFILES_ACTIVE=local +``` + +This way, each developer can use their own credentials without risk of leaking them to GitHub. + + +> ⚠️ **Common startup error**: +> ``` +> Failed to configure a DataSource: 'url' attribute is not specified +> and no embedded datasource could be configured. +> Reason: Failed to determine a suitable driver class +> ``` +> This error occurs because the PostgreSQL driver is present in the dependencies but the datasource URL is not configured. +> **Fix**: either add the `spring.datasource.*` properties above, or exclude the DataSource auto-configuration if no database is needed: +> ```properties +> spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration +> ``` --- @@ -231,11 +279,11 @@ This project consumes the public **Random User Generator** API: - [x] [Add Sonarqube in the project](https://github.com/XPEHO/spring_boot_java_random_user/issues/2) - [x] [Add PostgreSQL database with docker](https://github.com/XPEHO/spring_boot_java_random_user/issues/6) -- [ ] [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) -- [ ] [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) +- [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) +- [ ] [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/pom.xml b/pom.xml index 20b9212..e2a01c1 100644 --- a/pom.xml +++ b/pom.xml @@ -42,6 +42,11 @@ org.springframework.boot spring-boot-starter-actuator + + com.squareup.retrofit2 + converter-gson + 2.11.0 + org.springframework.boot spring-boot-starter-data-jdbc @@ -50,7 +55,6 @@ org.springframework.boot spring-boot-starter-webmvc - org.springdoc springdoc-openapi-starter-webmvc-ui diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/data/converters/UserConverter.java b/src/main/java/com/xpeho/spring_boot_java_random_user/data/converters/UserConverter.java new file mode 100644 index 0000000..92161c8 --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/converters/UserConverter.java @@ -0,0 +1,52 @@ +package com.xpeho.spring_boot_java_random_user.data.converters; +import com.xpeho.spring_boot_java_random_user.data.models.api.RandomUserResultDAO; +import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity; +import com.xpeho.spring_boot_java_random_user.data.models.db.User; +import org.springframework.stereotype.Service; + + +@Service +public class UserConverter { + // Domain -> DAO + public User toDao(UserEntity entity) { + User user = new User(); + user.setId(entity.id()); + user.setGender(entity.gender()); + user.setFirstname(entity.firstname()); + user.setLastname(entity.lastname()); + user.setCivility(entity.civility()); + user.setEmail(entity.email()); + user.setPhone(entity.phone()); + user.setPicture(entity.picture()); + user.setNationality(entity.nat()); + return user; + } + // DAO -> Domain + public UserEntity toDomain(User user) { + return new UserEntity( + user.getId(), + user.getGender(), + user.getFirstname(), + user.getLastname(), + user.getCivility(), + user.getEmail(), + user.getPhone(), + user.getPicture(), + user.getNationality() + ); + } + // API -> Domain + public UserEntity fromApiModel(RandomUserResultDAO model) { + return new UserEntity( + null, + model.getGender(), + model.getFirstName(), + model.getLastName(), + null, + model.getEmail(), + model.getPhone(), + model.getImage(), + null + ); + } +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserNameDAO.java b/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserNameDAO.java new file mode 100644 index 0000000..a095b9c --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserNameDAO.java @@ -0,0 +1,14 @@ +package com.xpeho.spring_boot_java_random_user.data.models.api; + +public class RandomUserNameDAO { + private String title; + private String first; + private String last; + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + public String getFirst() { return first; } + public void setFirst(String first) { this.first = first; } + public String getLast() { return last; } + public void setLast(String last) { this.last = last; } +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserPictureDAO.java b/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserPictureDAO.java new file mode 100644 index 0000000..9096ddb --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserPictureDAO.java @@ -0,0 +1,13 @@ +package com.xpeho.spring_boot_java_random_user.data.models.api; + +public class RandomUserPictureDAO { + private String medium; + + public String getMedium() { + return medium; + } + + public void setMedium(String medium) { + this.medium = medium; + } +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserResponse.java b/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserResponse.java new file mode 100644 index 0000000..603922a --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserResponse.java @@ -0,0 +1,17 @@ +package com.xpeho.spring_boot_java_random_user.data.models.api; + +import com.google.gson.annotations.SerializedName; +import java.util.List; + +public class RandomUserResponse { + @SerializedName("users") + private List users; + + public List getUsers() { + return users; + } + + public void setUsers(List users) { + this.users = users; + } +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserResultDAO.java b/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserResultDAO.java new file mode 100644 index 0000000..7d79a46 --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserResultDAO.java @@ -0,0 +1,23 @@ +package com.xpeho.spring_boot_java_random_user.data.models.api; + +public class RandomUserResultDAO { + private String gender; + private String firstName; + private String lastName; + private String email; + private String phone; + private String image; + + public String getGender() { return gender; } + public void setGender(String gender) { this.gender = gender; } + public String getFirstName() { return firstName; } + public void setFirstName(String firstName) { this.firstName = firstName; } + public String getLastName() { return lastName; } + public void setLastName(String lastName) { this.lastName = lastName; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + public String getImage() { return image; } + public void setImage(String image) { this.image = image; } +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/db/User.java b/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/db/User.java new file mode 100644 index 0000000..7351350 --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/db/User.java @@ -0,0 +1,45 @@ +package com.xpeho.spring_boot_java_random_user.data.models.db; + +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +@Table("users") +public class User { + @Id + private Long id; + private String gender; + @Column("firstname") + private String firstname; + @Column("lastname") + private String lastname; + private String civility; + private String email; + private String phone; + private String picture; + private String nationality; + + // Required by Spring Data JDBC to instantiate the entity via reflection + public User() { + // No initialization needed; fields are populated by Spring Data JDBC after instantiation + } + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getGender() { return gender; } + public void setGender(String gender) { this.gender = gender; } + public String getFirstname() { return firstname; } + public void setFirstname(String firstname) { this.firstname = firstname; } + public String getLastname() { return lastname; } + public void setLastname(String lastname) { this.lastname = lastname; } + public String getCivility() { return civility; } + public void setCivility(String civility) { this.civility = civility; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + public String getPicture() { return picture; } + public void setPicture(String picture) { this.picture = picture; } + public String getNationality() { return nationality; } + public void setNationality(String nationality) { this.nationality = nationality; } +} 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 new file mode 100644 index 0000000..3dae4e3 --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/services/UserServiceImpl.java @@ -0,0 +1,32 @@ +package com.xpeho.spring_boot_java_random_user.data.services; + +import com.xpeho.spring_boot_java_random_user.data.sources.database.UserRepository; +import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity; +import com.xpeho.spring_boot_java_random_user.domain.services.UserService; +import com.xpeho.spring_boot_java_random_user.data.models.db.User; +import com.xpeho.spring_boot_java_random_user.data.converters.UserConverter; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.StreamSupport; + +@Service + +public class UserServiceImpl implements UserService { + private final UserRepository userRepository; + private final UserConverter userConverter; + + public UserServiceImpl(UserRepository userRepository, UserConverter userConverter) { + this.userRepository = userRepository; + this.userConverter = userConverter; + } + + @Override + public List saveAll(List users) { + List daoUsers = users.stream().map(userConverter::toDao).toList(); + Iterable saved = userRepository.saveAll(daoUsers); + return StreamSupport.stream(saved.spliterator(), false) + .map(userConverter::toDomain) + .toList(); + } +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/data/sources/api/RandomUserApi.java b/src/main/java/com/xpeho/spring_boot_java_random_user/data/sources/api/RandomUserApi.java new file mode 100644 index 0000000..78eb496 --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/sources/api/RandomUserApi.java @@ -0,0 +1,11 @@ +package com.xpeho.spring_boot_java_random_user.data.sources.api; + +import com.xpeho.spring_boot_java_random_user.data.models.api.RandomUserResponse; +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; + +public interface RandomUserApi { + @GET("/users") + Call getRandomUsers(@Query("limit") int limit); +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/data/sources/api/RandomUserApiConfig.java b/src/main/java/com/xpeho/spring_boot_java_random_user/data/sources/api/RandomUserApiConfig.java new file mode 100644 index 0000000..da0c2f6 --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/sources/api/RandomUserApiConfig.java @@ -0,0 +1,27 @@ +package com.xpeho.spring_boot_java_random_user.data.sources.api; + +import okhttp3.OkHttpClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +@Configuration +public class RandomUserApiConfig { + @Bean + public Retrofit randomUserRetrofit(Environment env) { + String baseUrl = env.getProperty("randomuser.api.base-url", "https://dummyjson.com/"); + OkHttpClient client = new OkHttpClient.Builder().build(); + return new Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .client(client) + .build(); + } + + @Bean + public RandomUserApi randomUserApi(Retrofit randomUserRetrofit) { + return randomUserRetrofit.create(RandomUserApi.class); + } +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/data/sources/api/RandomUserProviderImpl.java b/src/main/java/com/xpeho/spring_boot_java_random_user/data/sources/api/RandomUserProviderImpl.java new file mode 100644 index 0000000..e30a5ea --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/sources/api/RandomUserProviderImpl.java @@ -0,0 +1,38 @@ +package com.xpeho.spring_boot_java_random_user.data.sources.api; + +import com.xpeho.spring_boot_java_random_user.data.models.api.RandomUserResponse; +import com.xpeho.spring_boot_java_random_user.data.models.api.RandomUserResultDAO; +import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity; +import com.xpeho.spring_boot_java_random_user.domain.services.RandomUserProvider; +import com.xpeho.spring_boot_java_random_user.data.converters.UserConverter; +import org.springframework.stereotype.Service; +import retrofit2.Response; + +import java.io.IOException; +import java.util.List; + +@Service +public class RandomUserProviderImpl implements RandomUserProvider { + private final RandomUserApi randomUserApi; + private final UserConverter userConverter; + + public RandomUserProviderImpl(RandomUserApi randomUserApi, UserConverter userConverter) { + this.randomUserApi = randomUserApi; + this.userConverter = userConverter; + } + + @Override + public List fetchRandomUsers(int count) throws IOException { + Response response = randomUserApi.getRandomUsers(count).execute(); + if (!response.isSuccessful() || response.body() == null) { + throw new IOException("Failed to fetch users: " + response.code()); + } + List users = response.body().getUsers(); + if (users == null) { + throw new IOException("Failed to parse users from response"); + } + return users.stream() + .map(userConverter::fromApiModel) + .toList(); + } +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/data/sources/database/UserRepository.java b/src/main/java/com/xpeho/spring_boot_java_random_user/data/sources/database/UserRepository.java new file mode 100644 index 0000000..11a75bb --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/sources/database/UserRepository.java @@ -0,0 +1,7 @@ +package com.xpeho.spring_boot_java_random_user.data.sources.database; + +import com.xpeho.spring_boot_java_random_user.data.models.db.User; +import org.springframework.data.repository.CrudRepository; + +public interface UserRepository extends CrudRepository { +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/domain/entities/UserEntity.java b/src/main/java/com/xpeho/spring_boot_java_random_user/domain/entities/UserEntity.java new file mode 100644 index 0000000..557327c --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/domain/entities/UserEntity.java @@ -0,0 +1,13 @@ +package com.xpeho.spring_boot_java_random_user.domain.entities; + +public record UserEntity( + Long id, + 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/services/RandomUserProvider.java b/src/main/java/com/xpeho/spring_boot_java_random_user/domain/services/RandomUserProvider.java new file mode 100644 index 0000000..6a785b9 --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/domain/services/RandomUserProvider.java @@ -0,0 +1,10 @@ +package com.xpeho.spring_boot_java_random_user.domain.services; + +import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity; +import java.io.IOException; +import java.util.List; + +public interface RandomUserProvider { + List fetchRandomUsers(int count) throws IOException; + +} 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 new file mode 100644 index 0000000..2462da7 --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/domain/services/UserService.java @@ -0,0 +1,8 @@ +package com.xpeho.spring_boot_java_random_user.domain.services; + +import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity; +import java.util.List; + +public interface UserService { + List saveAll(List users); +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/domain/usecases/FetchAndSaveRandomUsersUseCase.java b/src/main/java/com/xpeho/spring_boot_java_random_user/domain/usecases/FetchAndSaveRandomUsersUseCase.java new file mode 100644 index 0000000..f9a41ac --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/domain/usecases/FetchAndSaveRandomUsersUseCase.java @@ -0,0 +1,25 @@ +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.services.UserService; +import com.xpeho.spring_boot_java_random_user.domain.services.RandomUserProvider; +import org.springframework.stereotype.Service; +import java.io.IOException; +import java.util.List; + +@Service +public class FetchAndSaveRandomUsersUseCase { + + private final UserService userService; + private final RandomUserProvider randomUserProvider; + + public FetchAndSaveRandomUsersUseCase(UserService userService, RandomUserProvider randomUserProvider) { + this.userService = userService; + this.randomUserProvider = randomUserProvider; + } + + public List execute(int count) throws IOException { + List users = randomUserProvider.fetchRandomUsers(count); + return userService.saveAll(users); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..530f82d --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/presentation/controllers/UserController.java @@ -0,0 +1,40 @@ +package com.xpeho.spring_boot_java_random_user.presentation.controllers; + +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; + + +@RequestMapping("/random-users") +@Tag(name = "User", description = "Endpoints for random user generation") +public interface UserController { + + @GetMapping("") + @Operation( + summary = "Get random users", + description = "Fetches a list of random users from the external API and saves them to the database.", + parameters = { + @Parameter(name = "count", description = "Number of users to generate (max 5000)", example = "500") + } + ) + @ApiResponse(responseCode = "200", description = "List of users successfully fetched and saved") + @ApiResponse(responseCode = "500", description = "Internal server error") + @ApiResponse(responseCode = "503", description = "External service unavailable") + ResponseEntity> getRandomUsers( + @RequestParam(defaultValue = "500") + @Min(1) + @Max(5000) + int count + ); +} 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 new file mode 100644 index 0000000..50f860a --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/presentation/handlers/UserHandler.java @@ -0,0 +1,39 @@ +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.usecases.FetchAndSaveRandomUsersUseCase; +import com.xpeho.spring_boot_java_random_user.presentation.controllers.UserController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.util.List; + +import static java.util.Collections.emptyList; + +@RestController +public class UserHandler implements UserController { + + private static final Logger logger = LoggerFactory.getLogger(UserHandler.class); + + private final FetchAndSaveRandomUsersUseCase fetchAndSaveRandomUsersUseCase; + + public UserHandler(FetchAndSaveRandomUsersUseCase fetchAndSaveRandomUsersUseCase) { + this.fetchAndSaveRandomUsersUseCase = fetchAndSaveRandomUsersUseCase; + } + + @Override + public ResponseEntity> getRandomUsers(int count) { + try { + List users = fetchAndSaveRandomUsersUseCase.execute(count); + return ResponseEntity.ok(users); + } catch (IOException e) { + logger.error("Error fetching random users: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(emptyList()); + } + } +} \ No newline at end of file diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties new file mode 100644 index 0000000..2602ffc --- /dev/null +++ b/src/main/resources/application-local.properties @@ -0,0 +1,10 @@ + +spring.application.name=spring_boot_java_random_user +# Swagger UI custom path +springdoc.swagger-ui.path=/api +# URL for Random User API +randomuser.api.base-url=https://dummyjson.com/ + +spring.datasource.url=jdbc:postgresql://localhost:${POSTGRES_PORT}/${POSTGRES_DB} +spring.datasource.username=${POSTGRES_USER} +spring.datasource.password=${POSTGRES_PASSWORD} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index df2d4f1..ce63678 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,6 +3,9 @@ spring.application.name=spring_boot_java_random_user # Swagger UI custom path springdoc.swagger-ui.path=/api +# URL for Random User API +randomuser.api.base-url=https://dummyjson.com/ + # Database configuration spring.datasource.url=jdbc:postgresql://localhost:${POSTGRES_PORT}/${POSTGRES_DB} spring.datasource.username=${POSTGRES_USER} diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 2140b5c..9ccd248 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,6 +1,6 @@ -DROP TABLE IF EXISTS "user"; +DROP TABLE IF EXISTS "users"; -CREATE TABLE IF NOT EXISTS "user" +CREATE TABLE IF NOT EXISTS "users" ( id SERIAL PRIMARY KEY, gender VARCHAR(20), diff --git a/src/test/java/com/xpeho/spring_boot_java_random_user/SpringBootJavaRandomUserApplicationTests.java b/src/test/java/com/xpeho/spring_boot_java_random_user/SpringBootJavaRandomUserApplicationTests.java index 21bcb10..073dbe8 100644 --- a/src/test/java/com/xpeho/spring_boot_java_random_user/SpringBootJavaRandomUserApplicationTests.java +++ b/src/test/java/com/xpeho/spring_boot_java_random_user/SpringBootJavaRandomUserApplicationTests.java @@ -1,15 +1,30 @@ package com.xpeho.spring_boot_java_random_user; +import com.xpeho.spring_boot_java_random_user.data.sources.database.UserRepository; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest @ActiveProfiles("test") class SpringBootJavaRandomUserApplicationTests { + @Autowired + ApplicationContext applicationContext; + + @MockitoBean + UserRepository userRepository; + @Test - void contextLoads() { + @DisplayName("Spring context should load successfully") + void springContextShouldLoadSuccessfully() { + assertThat(applicationContext).isNotNull(); } } diff --git a/src/test/java/com/xpeho/spring_boot_java_random_user/data/converters/UserConverterTest.java b/src/test/java/com/xpeho/spring_boot_java_random_user/data/converters/UserConverterTest.java new file mode 100644 index 0000000..e2afd04 --- /dev/null +++ b/src/test/java/com/xpeho/spring_boot_java_random_user/data/converters/UserConverterTest.java @@ -0,0 +1,67 @@ +package com.xpeho.spring_boot_java_random_user.data.converters; + +import com.xpeho.spring_boot_java_random_user.data.models.api.RandomUserResultDAO; +import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity; +import com.xpeho.spring_boot_java_random_user.data.models.db.User; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class UserConverterTest { + private final UserConverter converter = new UserConverter(); + + @Test + @DisplayName("Should convert API model to domain entity with all fields") + void shouldConvertApiModelToDomainWithAllFields() { + RandomUserResultDAO api = new RandomUserResultDAO(); + api.setGender("male"); + api.setFirstName("John"); + api.setLastName("Doe"); + api.setEmail("john@doe.com"); + api.setPhone("1234"); + api.setImage("pic.jpg"); + UserEntity entity = converter.fromApiModel(api); + assertNull(entity.id()); + assertEquals("male", entity.gender()); + assertEquals("John", entity.firstname()); + assertEquals("Doe", entity.lastname()); + assertEquals("john@doe.com", entity.email()); + assertEquals("1234", entity.phone()); + assertEquals("pic.jpg", entity.picture()); + } + + @Test + @DisplayName("Should handle missing fields when converting API model") + void shouldHandleMissingFieldsFromApiModel() { + RandomUserResultDAO api = new RandomUserResultDAO(); + api.setGender("female"); + api.setEmail("jane@doe.com"); + api.setPhone("5678"); + UserEntity entity = converter.fromApiModel(api); + assertNull(entity.firstname()); + assertNull(entity.lastname()); + assertNull(entity.picture()); + assertEquals("female", entity.gender()); + assertEquals("jane@doe.com", entity.email()); + assertEquals("5678", entity.phone()); + } + + @Test + @DisplayName("Should convert domain to DB and back without losing data") + void shouldConvertDomainToDbAndBackWithoutLosingData() { + UserEntity entity = new UserEntity(1L, "male", "John", "Doe", "Mr", "john@doe.com", "1234", "pic.jpg", "FR"); + User dao = converter.toDao(entity); + assertEquals(entity.id(), dao.getId()); + assertEquals(entity.firstname(), dao.getFirstname()); + assertEquals(entity.lastname(), dao.getLastname()); + assertEquals(entity.email(), dao.getEmail()); + assertEquals(entity.picture(), dao.getPicture()); + UserEntity back = converter.toDomain(dao); + assertEquals(entity.id(), back.id()); + assertEquals(entity.firstname(), back.firstname()); + assertEquals(entity.lastname(), back.lastname()); + assertEquals(entity.email(), back.email()); + assertEquals(entity.picture(), back.picture()); + } +} diff --git a/src/test/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserNameDAOTest.java b/src/test/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserNameDAOTest.java new file mode 100644 index 0000000..63bc65a --- /dev/null +++ b/src/test/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserNameDAOTest.java @@ -0,0 +1,27 @@ +package com.xpeho.spring_boot_java_random_user.data.models.api; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class RandomUserNameDAOTest { + @Test + @DisplayName("Should store and return all name fields") + void shouldStoreAndReturnAllNameFields() { + RandomUserNameDAO name = new RandomUserNameDAO(); + name.setFirst("John"); + name.setLast("Doe"); + name.setTitle("Mr"); + assertEquals("John", name.getFirst()); + assertEquals("Doe", name.getLast()); + assertEquals("Mr", name.getTitle()); + } + @Test + @DisplayName("Should have null fields by default") + void shouldHaveNullFieldsByDefault() { + RandomUserNameDAO name = new RandomUserNameDAO(); + assertNull(name.getFirst()); + assertNull(name.getLast()); + assertNull(name.getTitle()); + } +} diff --git a/src/test/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserPictureDAOTest.java b/src/test/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserPictureDAOTest.java new file mode 100644 index 0000000..8ea3498 --- /dev/null +++ b/src/test/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserPictureDAOTest.java @@ -0,0 +1,21 @@ +package com.xpeho.spring_boot_java_random_user.data.models.api; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class RandomUserPictureDAOTest { + @Test + @DisplayName("Should store and return medium picture URL") + void shouldStoreAndReturnMediumPictureUrl() { + RandomUserPictureDAO picture = new RandomUserPictureDAO(); + picture.setMedium("pic.jpg"); + assertEquals("pic.jpg", picture.getMedium()); + } + @Test + @DisplayName("Should have null medium by default") + void shouldHaveNullMediumByDefault() { + RandomUserPictureDAO picture = new RandomUserPictureDAO(); + assertNull(picture.getMedium()); + } +} diff --git a/src/test/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserResponseTest.java b/src/test/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserResponseTest.java new file mode 100644 index 0000000..6fbbe64 --- /dev/null +++ b/src/test/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserResponseTest.java @@ -0,0 +1,24 @@ +package com.xpeho.spring_boot_java_random_user.data.models.api; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import java.util.List; +import static org.junit.jupiter.api.Assertions.*; + +class RandomUserResponseTest { + @Test + @DisplayName("Should store and return users list") + void shouldStoreAndReturnUsersList() { + RandomUserResponse response = new RandomUserResponse(); + List users = List.of(new RandomUserResultDAO(), new RandomUserResultDAO()); + response.setUsers(users); + assertEquals(users, response.getUsers()); + } + + @Test + @DisplayName("Should have null users list by default") + void shouldHaveNullUsersListByDefault() { + RandomUserResponse response = new RandomUserResponse(); + assertNull(response.getUsers()); + } +} diff --git a/src/test/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserResultDAOTest.java b/src/test/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserResultDAOTest.java new file mode 100644 index 0000000..1ba34ba --- /dev/null +++ b/src/test/java/com/xpeho/spring_boot_java_random_user/data/models/api/RandomUserResultDAOTest.java @@ -0,0 +1,37 @@ +package com.xpeho.spring_boot_java_random_user.data.models.api; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class RandomUserResultDAOTest { + @Test + @DisplayName("Should store and return all user fields") + void shouldStoreAndReturnAllUserFields() { + RandomUserResultDAO result = new RandomUserResultDAO(); + result.setGender("male"); + result.setFirstName("John"); + result.setLastName("Doe"); + result.setEmail("john@doe.com"); + result.setPhone("1234"); + result.setImage("pic.jpg"); + assertEquals("male", result.getGender()); + assertEquals("John", result.getFirstName()); + assertEquals("Doe", result.getLastName()); + assertEquals("john@doe.com", result.getEmail()); + assertEquals("1234", result.getPhone()); + assertEquals("pic.jpg", result.getImage()); + } + + @Test + @DisplayName("Should have null fields by default") + void shouldHaveNullFieldsByDefault() { + RandomUserResultDAO result = new RandomUserResultDAO(); + assertNull(result.getGender()); + assertNull(result.getFirstName()); + assertNull(result.getLastName()); + assertNull(result.getEmail()); + assertNull(result.getPhone()); + assertNull(result.getImage()); + } +} diff --git a/src/test/java/com/xpeho/spring_boot_java_random_user/data/sources/api/RandomUserProviderImplTest.java b/src/test/java/com/xpeho/spring_boot_java_random_user/data/sources/api/RandomUserProviderImplTest.java new file mode 100644 index 0000000..aa6cccf --- /dev/null +++ b/src/test/java/com/xpeho/spring_boot_java_random_user/data/sources/api/RandomUserProviderImplTest.java @@ -0,0 +1,71 @@ +package com.xpeho.spring_boot_java_random_user.data.sources.api; + +import com.xpeho.spring_boot_java_random_user.data.converters.UserConverter; +import com.xpeho.spring_boot_java_random_user.data.models.api.RandomUserResponse; +import com.xpeho.spring_boot_java_random_user.data.models.api.RandomUserResultDAO; +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 retrofit2.Call; +import retrofit2.Response; + +import java.io.IOException; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class RandomUserProviderImplTest { + private RandomUserApi randomUserApi; + private UserConverter userConverter; + private RandomUserProviderImpl provider; + private Call call; + + @BeforeEach + void setUp() { + randomUserApi = mock(RandomUserApi.class); + userConverter = mock(UserConverter.class); + call = mock(Call.class); + provider = new RandomUserProviderImpl(randomUserApi, userConverter); + } + + @Test + @DisplayName("Should fetch and convert users successfully") + void shouldFetchAndConvertUsersSuccessfully() throws IOException { + int count = 2; + RandomUserResponse responseObj = new RandomUserResponse(); + RandomUserResultDAO dao1 = new RandomUserResultDAO(); + RandomUserResultDAO dao2 = new RandomUserResultDAO(); + responseObj.setUsers(List.of(dao1, dao2)); + UserEntity entity1 = new UserEntity(null, "a", "b", "c", "d", "e", "f", "g", "h"); + UserEntity entity2 = new UserEntity(null, "i", "j", "k", "l", "m", "n", "o", "p"); + when(randomUserApi.getRandomUsers(count)).thenReturn(call); + when(call.execute()).thenReturn(Response.success(responseObj)); + when(userConverter.fromApiModel(dao1)).thenReturn(entity1); + when(userConverter.fromApiModel(dao2)).thenReturn(entity2); + + List result = provider.fetchRandomUsers(count); + assertEquals(2, result.size()); + assertEquals(entity1, result.get(0)); + assertEquals(entity2, result.get(1)); + } + + @Test + @DisplayName("Should throw IOException when API returns error") + void shouldThrowIOExceptionWhenApiReturnsError() throws IOException { + int count = 1; + when(randomUserApi.getRandomUsers(count)).thenReturn(call); + when(call.execute()).thenReturn(Response.error(500, okhttp3.ResponseBody.create(null, "error"))); + assertThrows(IOException.class, () -> provider.fetchRandomUsers(count)); + } + + @Test + @DisplayName("Should throw IOException when response body is null") + void shouldThrowIOExceptionWhenResponseBodyIsNull() throws IOException { + int count = 1; + when(randomUserApi.getRandomUsers(count)).thenReturn(call); + when(call.execute()).thenReturn(Response.success(null)); + assertThrows(IOException.class, () -> provider.fetchRandomUsers(count)); + } +} diff --git a/src/test/java/com/xpeho/spring_boot_java_random_user/domain/usecases/FetchAndSaveRandomUsersUseCaseTest.java b/src/test/java/com/xpeho/spring_boot_java_random_user/domain/usecases/FetchAndSaveRandomUsersUseCaseTest.java new file mode 100644 index 0000000..2648b0c --- /dev/null +++ b/src/test/java/com/xpeho/spring_boot_java_random_user/domain/usecases/FetchAndSaveRandomUsersUseCaseTest.java @@ -0,0 +1,67 @@ +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.services.RandomUserProvider; +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.io.IOException; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class FetchAndSaveRandomUsersUseCaseTest { + private UserService userService; + private RandomUserProvider randomUserProvider; + private FetchAndSaveRandomUsersUseCase useCase; + + @BeforeEach + void setUp() { + userService = mock(UserService.class); + randomUserProvider = mock(RandomUserProvider.class); + useCase = new FetchAndSaveRandomUsersUseCase(userService, randomUserProvider); + } + + @Test + @DisplayName("Should fetch users from API and save them") + void shouldFetchUsersFromApiAndSaveThem() throws IOException { + List fetched = List.of(new UserEntity( + 1L, "male", "John", "Doe", "Mr", "john@doe.com", "1234", "pic.jpg", "FR" + )); when(randomUserProvider.fetchRandomUsers(2)).thenReturn(fetched); + when(userService.saveAll(fetched)).thenReturn(fetched); + List result = useCase.execute(2); + assertEquals(fetched, result); + verify(randomUserProvider).fetchRandomUsers(2); + verify(userService).saveAll(fetched); + } + + @Test + @DisplayName("Should propagate IOException when API fails") + void shouldPropagateIOExceptionWhenApiFails() throws IOException { + when(randomUserProvider.fetchRandomUsers(1)).thenThrow(new IOException("API error")); + IOException ex = assertThrows(IOException.class, () -> useCase.execute(1)); + assertEquals("API error", ex.getMessage()); + } + + @Test + @DisplayName("Should handle null response gracefully") + void shouldHandleNullResponseGracefully() throws IOException { + when(randomUserProvider.fetchRandomUsers(1)).thenReturn(null); + when(userService.saveAll(null)).thenReturn(null); + List result = useCase.execute(1); + assertNull(result); + } + + @Test + @DisplayName("Should return empty list when no users found") + void shouldReturnEmptyListWhenNoUsersFound() throws IOException { + when(randomUserProvider.fetchRandomUsers(0)).thenReturn(Collections.emptyList()); + when(userService.saveAll(Collections.emptyList())).thenReturn(Collections.emptyList()); + List result = useCase.execute(0); + assertTrue(result.isEmpty()); + } +}