diff --git a/.github/workflows/sonar.yaml b/.github/workflows/sonar.yaml index 7b02547..da44c69 100644 --- a/.github/workflows/sonar.yaml +++ b/.github/workflows/sonar.yaml @@ -38,5 +38,21 @@ jobs: SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} run: | mkdir -p src/test/resources - echo ${{ secrets.APPLICATION_TEST_PROPERTIES }} | base64 -d > src/test/resources/application-test.properties - mvn clean verify sonar:sonar -Dsonar.qualitygate.wait=true + if [ -n "${{ secrets.APPLICATION_TEST_PROPERTIES }}" ]; then + if printf '%s' "${{ secrets.APPLICATION_TEST_PROPERTIES }}" | base64 --decode > src/test/resources/application-test.properties 2>/dev/null; then + echo "Loaded APPLICATION_TEST_PROPERTIES as base64" + else + printf '%s\n' "${{ secrets.APPLICATION_TEST_PROPERTIES }}" > src/test/resources/application-test.properties + echo "Loaded APPLICATION_TEST_PROPERTIES as raw text" + fi + else + : > src/test/resources/application-test.properties + fi + { + echo "spring.datasource.url=jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE" + echo "spring.datasource.driver-class-name=org.h2.Driver" + echo "spring.datasource.username=sa" + echo "spring.datasource.password=" + echo "spring.sql.init.mode=never" + } >> src/test/resources/application-test.properties + ./mvnw clean verify sonar:sonar -Dsonar.qualitygate.wait=true diff --git a/.gitignore b/.gitignore index 667aaef..0f24cd9 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ build/ ### VS Code ### .vscode/ + +# Test configuration generated in CI +src/test/resources/application-test.properties diff --git a/README.md b/README.md index 02b7578..4516114 100644 --- a/README.md +++ b/README.md @@ -123,9 +123,10 @@ Run unit and integration tests: ./mvnw test ``` -Tests use **Testcontainers** to spin up ephemeral Docker containers for external dependencies (e.g. PostgreSQL). +`SpringBootJavaRandomUserApplicationTests` uses the `test` profile (`application-test.properties`). +In CI, this file is generated by the Sonar workflow and overridden to use H2 in-memory settings. -> **Prerequisite for tests**: Docker must be installed and running. +> **Note**: Docker is only required for tests that explicitly use Testcontainers. --- @@ -136,7 +137,10 @@ A GitHub Actions workflow is configured in: ```bash .github/workflows/sonar.yaml ``` - +You need to add your own : +```bash +src/test/resources/application-test.properties +``` ### Workflow triggers - `push` on all branches @@ -148,9 +152,31 @@ A GitHub Actions workflow is configured in: - Maven build + tests + SonarQube analysis: ```bash -mvn clean verify sonar:sonar +./mvnw clean verify sonar:sonar -Dsonar.qualitygate.wait=true ``` +Before running tests, the workflow recreates: + +```bash +src/test/resources/application-test.properties +``` +spring.datasource.url=jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username= +spring.datasource.password= +spring.sql.init.mode=never + +and also a github secret named APPLICATION_TEST_PROPERTIES +with the content of your application-test.properties encoded in base64 + +### Required GitHub Secrets + +- `SONAR_TOKEN` +- `SONAR_HOST_URL` +- `APPLICATION_TEST_PROPERTIES` (`application-test.properties` content, Base64 or raw text) + +> `GITHUB_TOKEN` is provided automatically by GitHub Actions. + ### Generate the JaCoCo coverage report locally Run: @@ -196,7 +222,7 @@ 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) - [ ] [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) +- [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) diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..7ec4aaa --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,26 @@ +services: + postgres: + image: postgres:15-alpine + container_name: xpeho_postgres + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: xpeho_db + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./src/main/resources/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql + # INIT DATA + # - ./src/main/resources/data.sql:/docker-entrypoint-initdb.d/02-data.sql + networks: + - xpeho_network + +volumes: + postgres_data: + driver: local + +networks: + xpeho_network: + driver: bridge + diff --git a/pom.xml b/pom.xml index bac59da..de7a5f7 100644 --- a/pom.xml +++ b/pom.xml @@ -32,19 +32,28 @@ ${project.build.directory}/site/jacoco/jacoco.xml + + com.h2database + h2 + test + org.springframework.boot spring-boot-starter-actuator - - - - + + com.squareup.retrofit2 + converter-gson + 2.9.0 + + + org.springframework.boot + spring-boot-starter-data-jdbc + 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..6112c2b --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/converters/UserConverter.java @@ -0,0 +1,21 @@ +package com.xpeho.spring_boot_java_random_user.data.converters; + +import com.xpeho.spring_boot_java_random_user.data.models.RandomUserResultDAO; +import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity; +import org.springframework.stereotype.Service; + +@Service +public class UserConverter { + public UserEntity modelToEntity(RandomUserResultDAO model) { + UserEntity entity = new UserEntity(); + entity.setGender(model.gender); + entity.setFirstname(model.name.first); + entity.setLastname(model.name.last); + entity.setCivility(model.name.title); + entity.setEmail(model.email); + entity.setPhone(model.phone); + entity.setPicture(model.picture.medium); + entity.setNat(model.nat); + return entity; + } +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/RandomUserInfoDAO.java b/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/RandomUserInfoDAO.java new file mode 100644 index 0000000..f1ffe1a --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/RandomUserInfoDAO.java @@ -0,0 +1,8 @@ +package com.xpeho.spring_boot_java_random_user.data.models; + +public class RandomUserInfoDAO { + public String seed; + public int results; + public int page; + public String version; +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/RandomUserNameDAO.java b/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/RandomUserNameDAO.java new file mode 100644 index 0000000..a3298ad --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/RandomUserNameDAO.java @@ -0,0 +1,7 @@ +package com.xpeho.spring_boot_java_random_user.data.models; + +public class RandomUserNameDAO { + public String title; + public String first; + public String last; +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/RandomUserPictureDAO.java b/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/RandomUserPictureDAO.java new file mode 100644 index 0000000..514da2d --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/RandomUserPictureDAO.java @@ -0,0 +1,5 @@ +package com.xpeho.spring_boot_java_random_user.data.models; + +public class RandomUserPictureDAO { + public String medium; +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/RandomUserResponseDAO.java b/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/RandomUserResponseDAO.java new file mode 100644 index 0000000..19e70a3 --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/RandomUserResponseDAO.java @@ -0,0 +1,6 @@ +package com.xpeho.spring_boot_java_random_user.data.models; + +public class RandomUserResponseDAO { + public RandomUserResultDAO[] results; + public RandomUserInfoDAO info; +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/RandomUserResultDAO.java b/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/RandomUserResultDAO.java new file mode 100644 index 0000000..5db847b --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/models/RandomUserResultDAO.java @@ -0,0 +1,10 @@ +package com.xpeho.spring_boot_java_random_user.data.models; + +public class RandomUserResultDAO { + public String gender; + public RandomUserNameDAO name; + public String email; + public String phone; + public RandomUserPictureDAO picture; + public String nat; +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/data/repositories/UserRepository.java b/src/main/java/com/xpeho/spring_boot_java_random_user/data/repositories/UserRepository.java new file mode 100644 index 0000000..6357784 --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/repositories/UserRepository.java @@ -0,0 +1,7 @@ +package com.xpeho.spring_boot_java_random_user.data.repositories; + +import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity; +import org.springframework.data.repository.CrudRepository; + +public interface UserRepository extends CrudRepository { +} 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..47374ed --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/services/UserServiceImpl.java @@ -0,0 +1,24 @@ +package com.xpeho.spring_boot_java_random_user.data.services; + +import com.xpeho.spring_boot_java_random_user.data.repositories.UserRepository; +import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity; +import com.xpeho.spring_boot_java_random_user.domain.services.UserService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class UserServiceImpl implements UserService { + private final UserRepository userRepository; + + public UserServiceImpl(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public List saveAll(List users) { + Iterable saved = userRepository.saveAll(users); + return saved instanceof List ? (List) saved : + java.util.stream.StreamSupport.stream(saved.spliterator(), false).toList(); + } +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/data/sources/RandomUserApi.java b/src/main/java/com/xpeho/spring_boot_java_random_user/data/sources/RandomUserApi.java new file mode 100644 index 0000000..f0eb6f3 --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/sources/RandomUserApi.java @@ -0,0 +1,11 @@ +package com.xpeho.spring_boot_java_random_user.data.sources; + +import com.xpeho.spring_boot_java_random_user.data.models.RandomUserResponseDAO; +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; + +public interface RandomUserApi { + @GET("/api/") + Call getRandomUsers(@Query("results") int results); +} diff --git a/src/main/java/com/xpeho/spring_boot_java_random_user/data/sources/RandomUserRetrofitClient.java b/src/main/java/com/xpeho/spring_boot_java_random_user/data/sources/RandomUserRetrofitClient.java new file mode 100644 index 0000000..98591e6 --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/data/sources/RandomUserRetrofitClient.java @@ -0,0 +1,27 @@ +package com.xpeho.spring_boot_java_random_user.data.sources; + +import okhttp3.OkHttpClient; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +@Component +public class RandomUserRetrofitClient { + private final RandomUserApi apiInstance; + + public RandomUserRetrofitClient(Environment env) { + String baseUrl = env.getProperty("randomuser.api.base-url", "https://randomuser.me/"); + OkHttpClient client = new OkHttpClient.Builder().build(); + Retrofit retrofit = new Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .client(client) + .build(); + this.apiInstance = retrofit.create(RandomUserApi.class); + } + + public RandomUserApi getApi() { + return apiInstance; + } +} 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..ebe1a60 --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/domain/entities/UserEntity.java @@ -0,0 +1,36 @@ +package com.xpeho.spring_boot_java_random_user.domain.entities; + +import org.springframework.data.annotation.Id; + +public class UserEntity { + @Id + private Long id; + private String gender; + private String firstname; + private String lastname; + private String civility; + private String email; + private String phone; + private String picture; + private String nat; + + // Getters and setters + 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 getNat() { return nat; } + public void setNat(String nat) { this.nat = nat; } +} 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..b8b1437 --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/domain/usecases/FetchAndSaveRandomUsersUseCase.java @@ -0,0 +1,44 @@ +package com.xpeho.spring_boot_java_random_user.domain.usecases; + +import com.xpeho.spring_boot_java_random_user.data.models.RandomUserResponseDAO; +import com.xpeho.spring_boot_java_random_user.data.sources.RandomUserRetrofitClient; +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.converters.UserConverter; +import org.springframework.stereotype.Service; +import retrofit2.Response; + +import java.io.IOException; +import java.util.List; +import java.util.Arrays; + +@Service +public class FetchAndSaveRandomUsersUseCase { + + private final UserService userService; + private final UserConverter userConverter; + private final RandomUserRetrofitClient randomUserRetrofitClient; + + public FetchAndSaveRandomUsersUseCase(UserService userService, UserConverter userConverter, RandomUserRetrofitClient randomUserRetrofitClient) { + this.userService = userService; + this.userConverter = userConverter; + this.randomUserRetrofitClient = randomUserRetrofitClient; + } + + public List execute(int count) throws IOException { + Response response = randomUserRetrofitClient.getApi() + .getRandomUsers(count) + .execute(); + + if (!response.isSuccessful() || response.body() == null) { + throw new IOException("Failed to fetch users: " + response.code()); + } + + // Utilisation des Streams pour la transformation + List users = Arrays.stream(response.body().results) + .map(userConverter::modelToEntity) + .toList(); + + 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..122b785 --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/presentation/controllers/UserController.java @@ -0,0 +1,35 @@ +package com.xpeho.spring_boot_java_random_user.presentation.controllers; + +import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity; +import org.springframework.http.ResponseEntity; +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.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +import java.util.List; + +@RequestMapping("/user") +@Tag(name = "User", description = "Endpoints for random user generation") +public interface UserController { + + @GetMapping("/random") + @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", example = "500") + } + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "List of users successfully fetched and saved"), + @ApiResponse(responseCode = "500", description = "Internal server error") + }) + ResponseEntity> getRandomUsers(@RequestParam(defaultValue = "500") 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..717adf8 --- /dev/null +++ b/src/main/java/com/xpeho/spring_boot_java_random_user/presentation/handlers/UserHandler.java @@ -0,0 +1,29 @@ +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.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.util.List; + +@RestController +public class UserHandler implements UserController { + 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) { + return ResponseEntity.internalServerError().build(); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 16574d6..745945b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,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://randomuser.me/ + +spring.datasource.url=jdbc:postgresql://localhost:5432/totodb +spring.datasource.username=postgres +spring.datasource.password=postgres \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..ab88ef5 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS user_entity ( + id SERIAL PRIMARY KEY, + gender VARCHAR(10), + firstname VARCHAR(100), + lastname VARCHAR(100), + civility VARCHAR(20), + email VARCHAR(255), + phone VARCHAR(50), + picture VARCHAR(255), + nat VARCHAR(10) +); 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 367a822..21bcb10 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 @@ -2,9 +2,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; @SpringBootTest +@ActiveProfiles("test") class SpringBootJavaRandomUserApplicationTests { @Test