Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions .github/workflows/sonar.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ build/

### VS Code ###
.vscode/

# Test configuration generated in CI
src/test/resources/application-test.properties
36 changes: 31 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand All @@ -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
Expand All @@ -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=<your_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:
Expand Down Expand Up @@ -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)
Expand Down
26 changes: 26 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -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

19 changes: 14 additions & 5 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,28 @@
<sonar.coverage.jacoco.xmlReportPaths>${project.build.directory}/site/jacoco/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths>
</properties>
<dependencies>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-data-jdbc</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-gson</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>

<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.xpeho.spring_boot_java_random_user.data.models;

public class RandomUserPictureDAO {
public String medium;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.xpeho.spring_boot_java_random_user.data.models;

public class RandomUserResponseDAO {
public RandomUserResultDAO[] results;
public RandomUserInfoDAO info;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<UserEntity, Long> {
}
Original file line number Diff line number Diff line change
@@ -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<UserEntity> saveAll(List<UserEntity> users) {
Iterable<UserEntity> saved = userRepository.saveAll(users);
return saved instanceof List<UserEntity> ? (List<UserEntity>) saved :
java.util.stream.StreamSupport.stream(saved.spliterator(), false).toList();
}
}
Original file line number Diff line number Diff line change
@@ -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<RandomUserResponseDAO> getRandomUsers(@Query("results") int results);
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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; }
}
Original file line number Diff line number Diff line change
@@ -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<UserEntity> saveAll(List<UserEntity> users);
}
Original file line number Diff line number Diff line change
@@ -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<UserEntity> execute(int count) throws IOException {
Response<RandomUserResponseDAO> 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<UserEntity> users = Arrays.stream(response.body().results)
.map(userConverter::modelToEntity)
.toList();

return userService.saveAll(users);
}
}
Loading
Loading