feat(auth): POST /auth/login retourne email, name, role et token
CI — Tests & Docker Build / Tests (push) Failing after 4s
CI — Tests & Docker Build / Build & push image Docker (push) Has been skipped

- Champ `name` ajouté sur User, UserEntity, RegisterRequest
- AuthenticateUserUseCase retourne Result(user, token) au lieu du token seul
- UserNotFoundException remplacé par BadCredentialsException au login (pas de fuite d'info)
- @Email retiré de LoginRequest (identifiant = "gato", pas nécessairement un email)
- Migration V2 : colonne name + utilisateur par défaut gato/change (ADMIN)
- bytecode cible Java 21 (ASM Spring Boot 3.4 ne supporte pas Java 25)
- Tests : AbstractIntegrationTest simplifié, URL TC JDBC + network host

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 08:24:41 +02:00
parent c34cc41496
commit 12a28af1ca
19 changed files with 64 additions and 42 deletions
+9 -2
View File
@@ -29,17 +29,24 @@ Voir `java-env.md` (règles workspace) pour les contraintes. Commande standard :
podman run --rm -v $(pwd):/workspace:Z -w /workspace eclipse-temurin:25-jdk ./gradlew test --no-daemon podman run --rm -v $(pwd):/workspace:Z -w /workspace eclipse-temurin:25-jdk ./gradlew test --no-daemon
``` ```
Testcontainers nécessite le socket Docker/Podman : Testcontainers nécessite le socket Podman et le réseau hôte (DinD) :
```bash ```bash
podman run --rm \ podman run --rm \
--privileged \
--network host \
-v $(pwd):/workspace:Z \ -v $(pwd):/workspace:Z \
-v /run/user/$(id -u)/podman/podman.sock:/var/run/docker.sock \ -v /run/user/$(id -u)/podman/podman.sock:/var/run/docker.sock:Z \
-e DOCKER_HOST=unix:///var/run/docker.sock \
-e TESTCONTAINERS_RYUK_DISABLED=true \
-w /workspace \ -w /workspace \
eclipse-temurin:25-jdk \ eclipse-temurin:25-jdk \
./gradlew test --no-daemon ./gradlew test --no-daemon
``` ```
- `--network host` est obligatoire : le TC JDBC driver démarre PostgreSQL via le socket Podman et s'y connecte via `localhost:PORT` — sans host network, ce port n'est pas accessible depuis l'intérieur du container.
- `TESTCONTAINERS_RYUK_DISABLED=true` : Ryuk (le garbage collector de Testcontainers) ne fonctionne pas en DinD.
## JaCoCo dans build.gradle ## JaCoCo dans build.gradle
```groovy ```groovy
+6
View File
@@ -13,6 +13,12 @@ java {
} }
} }
// Spring Boot 3.4.x embarque ASM 9.7 qui supporte jusqu'au bytecode Java 23 (version 67).
// On compile avec le JDK 25 mais on cible Java 21 pour rester compatible avec le scanner Spring.
tasks.withType(JavaCompile).configureEach {
options.release = 21
}
repositories { repositories {
mavenCentral() mavenCentral()
} }
@@ -1,7 +1,10 @@
package com.olhar.olharapi.application.port.in; package com.olhar.olharapi.application.port.in;
import com.olhar.olharapi.domain.model.User;
public interface AuthenticateUserUseCase { public interface AuthenticateUserUseCase {
String authenticate(Command command); Result authenticate(Command command);
record Command(String email, String password) {} record Command(String email, String password) {}
record Result(User user, String token) {}
} }
@@ -5,5 +5,5 @@ import com.olhar.olharapi.domain.model.User;
public interface RegisterUserUseCase { public interface RegisterUserUseCase {
User register(Command command); User register(Command command);
record Command(String email, String password) {} record Command(String email, String name, String password) {}
} }
@@ -2,7 +2,6 @@ package com.olhar.olharapi.application.usecase;
import com.olhar.olharapi.application.port.in.AuthenticateUserUseCase; import com.olhar.olharapi.application.port.in.AuthenticateUserUseCase;
import com.olhar.olharapi.application.port.out.UserRepository; import com.olhar.olharapi.application.port.out.UserRepository;
import com.olhar.olharapi.domain.exception.UserNotFoundException;
import com.olhar.olharapi.domain.model.User; import com.olhar.olharapi.domain.model.User;
import com.olhar.olharapi.infrastructure.security.JwtService; import com.olhar.olharapi.infrastructure.security.JwtService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -19,14 +18,14 @@ public class AuthenticateUserService implements AuthenticateUserUseCase {
private final JwtService jwtService; private final JwtService jwtService;
@Override @Override
public String authenticate(Command command) { public Result authenticate(Command command) {
User user = userRepository.findByEmail(command.email()) User user = userRepository.findByEmail(command.email())
.orElseThrow(() -> new UserNotFoundException(command.email())); .orElseThrow(() -> new BadCredentialsException("Identifiants invalides"));
if (!passwordEncoder.matches(command.password(), user.passwordHash())) { if (!passwordEncoder.matches(command.password(), user.passwordHash())) {
throw new BadCredentialsException("Invalid credentials"); throw new BadCredentialsException("Identifiants invalides");
} }
return jwtService.generateToken(user); return new Result(user, jwtService.generateToken(user));
} }
} }
@@ -29,6 +29,7 @@ public class RegisterUserService implements RegisterUserUseCase {
User user = new User( User user = new User(
UUID.randomUUID(), UUID.randomUUID(),
command.email(), command.email(),
command.name(),
passwordEncoder.encode(command.password()), passwordEncoder.encode(command.password()),
User.Role.USER, User.Role.USER,
Instant.now() Instant.now()
@@ -6,11 +6,16 @@ import java.util.UUID;
public record User( public record User(
UUID id, UUID id,
String email, String email,
String name,
String passwordHash, String passwordHash,
Role role, Role role,
Instant createdAt Instant createdAt
) { ) {
public enum Role { public enum Role {
USER, ADMIN USER, ADMIN;
public String toApiRole() {
return this == ADMIN ? "admin" : "member";
}
} }
} }
@@ -21,6 +21,9 @@ public class UserEntity {
@Column(nullable = false, unique = true) @Column(nullable = false, unique = true)
private String email; private String email;
@Column(nullable = false)
private String name;
@Column(name = "password_hash", nullable = false) @Column(name = "password_hash", nullable = false)
private String passwordHash; private String passwordHash;
@@ -11,6 +11,7 @@ public class UserPersistenceMapper {
return new User( return new User(
entity.getId(), entity.getId(),
entity.getEmail(), entity.getEmail(),
entity.getName(),
entity.getPasswordHash(), entity.getPasswordHash(),
User.Role.valueOf(entity.getRole().name()), User.Role.valueOf(entity.getRole().name()),
entity.getCreatedAt() entity.getCreatedAt()
@@ -21,6 +22,7 @@ public class UserPersistenceMapper {
return UserEntity.builder() return UserEntity.builder()
.id(user.id()) .id(user.id())
.email(user.email()) .email(user.email())
.name(user.name())
.passwordHash(user.passwordHash()) .passwordHash(user.passwordHash())
.role(UserEntity.RoleEnum.valueOf(user.role().name())) .role(UserEntity.RoleEnum.valueOf(user.role().name()))
.createdAt(user.createdAt()) .createdAt(user.createdAt())
@@ -30,7 +30,7 @@ public class AuthController {
@Operation(summary = "Créer un compte utilisateur") @Operation(summary = "Créer un compte utilisateur")
public UserResponse register(@Valid @RequestBody RegisterRequest request) { public UserResponse register(@Valid @RequestBody RegisterRequest request) {
User user = registerUserUseCase.register( User user = registerUserUseCase.register(
new RegisterUserUseCase.Command(request.email(), request.password()) new RegisterUserUseCase.Command(request.email(), request.name(), request.password())
); );
return userRestMapper.toResponse(user); return userRestMapper.toResponse(user);
} }
@@ -38,9 +38,10 @@ public class AuthController {
@PostMapping("/login") @PostMapping("/login")
@Operation(summary = "S'authentifier et obtenir un token JWT") @Operation(summary = "S'authentifier et obtenir un token JWT")
public AuthResponse login(@Valid @RequestBody LoginRequest request) { public AuthResponse login(@Valid @RequestBody LoginRequest request) {
String token = authenticateUserUseCase.authenticate( AuthenticateUserUseCase.Result result = authenticateUserUseCase.authenticate(
new AuthenticateUserUseCase.Command(request.email(), request.password()) new AuthenticateUserUseCase.Command(request.email(), request.password())
); );
return new AuthResponse(token); User user = result.user();
return new AuthResponse(user.email(), user.name(), user.role().toApiRole(), result.token());
} }
} }
@@ -4,6 +4,6 @@ import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
public record LoginRequest( public record LoginRequest(
@Email @NotBlank String email, @NotBlank String email,
@NotBlank String password @NotBlank String password
) {} ) {}
@@ -6,5 +6,6 @@ import jakarta.validation.constraints.Size;
public record RegisterRequest( public record RegisterRequest(
@Email @NotBlank String email, @Email @NotBlank String email,
@NotBlank String name,
@NotBlank @Size(min = 8) String password @NotBlank @Size(min = 8) String password
) {} ) {}
@@ -1,3 +1,3 @@
package com.olhar.olharapi.interfaces.rest.dto.response; package com.olhar.olharapi.interfaces.rest.dto.response;
public record AuthResponse(String token) {} public record AuthResponse(String email, String name, String role, String token) {}
@@ -3,4 +3,4 @@ package com.olhar.olharapi.interfaces.rest.dto.response;
import java.time.Instant; import java.time.Instant;
import java.util.UUID; import java.util.UUID;
public record UserResponse(UUID id, String email, String role, Instant createdAt) {} public record UserResponse(UUID id, String email, String name, String role, Instant createdAt) {}
@@ -11,7 +11,8 @@ public class UserRestMapper {
return new UserResponse( return new UserResponse(
user.id(), user.id(),
user.email(), user.email(),
user.role().name(), user.name(),
user.role().toApiRole(),
user.createdAt() user.createdAt()
); );
} }
@@ -0,0 +1,11 @@
ALTER TABLE users ADD COLUMN name VARCHAR(255) NOT NULL DEFAULT '';
INSERT INTO users (id, email, name, password_hash, role, created_at)
VALUES (
gen_random_uuid(),
'gato',
'Gato',
'$2a$10$53jNzmKK21BVNrhrAToFouWoO/agW3TVe8j7nusbrYRVqALp/MALK',
'ADMIN',
now()
) ON CONFLICT (email) DO NOTHING;
@@ -2,28 +2,10 @@ package com.olhar.olharapi.infrastructure;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test") @ActiveProfiles("test")
@Testcontainers
public abstract class AbstractIntegrationTest { public abstract class AbstractIntegrationTest {
// Le container PostgreSQL est géré automatiquement via l'URL TC JDBC
@Container // définie dans application-test.yml : jdbc:tc:postgresql:16:///olhar_test
static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
.withDatabaseName("olhar_test")
.withUsername("olhar")
.withPassword("olhar");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
registry.add("spring.datasource.driver-class-name", () -> "org.postgresql.Driver");
}
} }
@@ -23,7 +23,7 @@ class JwtServiceTest {
@Test @Test
void generateToken_shouldReturnValidToken() { void generateToken_shouldReturnValidToken() {
User user = new User(UUID.randomUUID(), "user@example.com", "hash", User.Role.USER, Instant.now()); User user = new User(UUID.randomUUID(), "user@example.com", "Test User", "hash", User.Role.USER, Instant.now());
String token = jwtService.generateToken(user); String token = jwtService.generateToken(user);
@@ -18,7 +18,7 @@ class AuthControllerIT extends AbstractIntegrationTest {
@Test @Test
void register_shouldCreateUser_whenEmailIsNew() { void register_shouldCreateUser_whenEmailIsNew() {
RegisterRequest request = new RegisterRequest("test@example.com", "password123"); RegisterRequest request = new RegisterRequest("test@example.com", "Test User", "password123");
ResponseEntity<String> response = restTemplate.postForEntity("/api/v1/auth/register", request, String.class); ResponseEntity<String> response = restTemplate.postForEntity("/api/v1/auth/register", request, String.class);
@@ -28,7 +28,7 @@ class AuthControllerIT extends AbstractIntegrationTest {
@Test @Test
void register_shouldReturnConflict_whenEmailAlreadyExists() { void register_shouldReturnConflict_whenEmailAlreadyExists() {
RegisterRequest request = new RegisterRequest("duplicate@example.com", "password123"); RegisterRequest request = new RegisterRequest("duplicate@example.com", "Duplicate User", "password123");
restTemplate.postForEntity("/api/v1/auth/register", request, String.class); restTemplate.postForEntity("/api/v1/auth/register", request, String.class);
ResponseEntity<String> response = restTemplate.postForEntity("/api/v1/auth/register", request, String.class); ResponseEntity<String> response = restTemplate.postForEntity("/api/v1/auth/register", request, String.class);
@@ -38,7 +38,7 @@ class AuthControllerIT extends AbstractIntegrationTest {
@Test @Test
void login_shouldReturnToken_whenCredentialsAreValid() { void login_shouldReturnToken_whenCredentialsAreValid() {
RegisterRequest registerRequest = new RegisterRequest("login@example.com", "password123"); RegisterRequest registerRequest = new RegisterRequest("login@example.com", "Login User", "password123");
restTemplate.postForEntity("/api/v1/auth/register", registerRequest, String.class); restTemplate.postForEntity("/api/v1/auth/register", registerRequest, String.class);
LoginRequest loginRequest = new LoginRequest("login@example.com", "password123"); LoginRequest loginRequest = new LoginRequest("login@example.com", "password123");
@@ -50,7 +50,7 @@ class AuthControllerIT extends AbstractIntegrationTest {
@Test @Test
void login_shouldReturnUnauthorized_whenPasswordIsWrong() { void login_shouldReturnUnauthorized_whenPasswordIsWrong() {
RegisterRequest registerRequest = new RegisterRequest("wrongpass@example.com", "password123"); RegisterRequest registerRequest = new RegisterRequest("wrongpass@example.com", "Wrong Pass User", "password123");
restTemplate.postForEntity("/api/v1/auth/register", registerRequest, String.class); restTemplate.postForEntity("/api/v1/auth/register", registerRequest, String.class);
LoginRequest loginRequest = new LoginRequest("wrongpass@example.com", "wrongpassword"); LoginRequest loginRequest = new LoginRequest("wrongpass@example.com", "wrongpassword");