feat(auth): POST /auth/login retourne email, name, role et token
- 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:
@@ -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
|
||||
```
|
||||
|
||||
Testcontainers nécessite le socket Docker/Podman :
|
||||
Testcontainers nécessite le socket Podman et le réseau hôte (DinD) :
|
||||
|
||||
```bash
|
||||
podman run --rm \
|
||||
--privileged \
|
||||
--network host \
|
||||
-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 \
|
||||
eclipse-temurin:25-jdk \
|
||||
./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
|
||||
|
||||
```groovy
|
||||
|
||||
@@ -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 {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.olhar.olharapi.application.port.in;
|
||||
|
||||
import com.olhar.olharapi.domain.model.User;
|
||||
|
||||
public interface AuthenticateUserUseCase {
|
||||
String authenticate(Command command);
|
||||
Result authenticate(Command command);
|
||||
|
||||
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 {
|
||||
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.out.UserRepository;
|
||||
import com.olhar.olharapi.domain.exception.UserNotFoundException;
|
||||
import com.olhar.olharapi.domain.model.User;
|
||||
import com.olhar.olharapi.infrastructure.security.JwtService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -19,14 +18,14 @@ public class AuthenticateUserService implements AuthenticateUserUseCase {
|
||||
private final JwtService jwtService;
|
||||
|
||||
@Override
|
||||
public String authenticate(Command command) {
|
||||
public Result authenticate(Command command) {
|
||||
User user = userRepository.findByEmail(command.email())
|
||||
.orElseThrow(() -> new UserNotFoundException(command.email()));
|
||||
.orElseThrow(() -> new BadCredentialsException("Identifiants invalides"));
|
||||
|
||||
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(
|
||||
UUID.randomUUID(),
|
||||
command.email(),
|
||||
command.name(),
|
||||
passwordEncoder.encode(command.password()),
|
||||
User.Role.USER,
|
||||
Instant.now()
|
||||
|
||||
@@ -6,11 +6,16 @@ import java.util.UUID;
|
||||
public record User(
|
||||
UUID id,
|
||||
String email,
|
||||
String name,
|
||||
String passwordHash,
|
||||
Role role,
|
||||
Instant createdAt
|
||||
) {
|
||||
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)
|
||||
private String email;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(name = "password_hash", nullable = false)
|
||||
private String passwordHash;
|
||||
|
||||
|
||||
+2
@@ -11,6 +11,7 @@ public class UserPersistenceMapper {
|
||||
return new User(
|
||||
entity.getId(),
|
||||
entity.getEmail(),
|
||||
entity.getName(),
|
||||
entity.getPasswordHash(),
|
||||
User.Role.valueOf(entity.getRole().name()),
|
||||
entity.getCreatedAt()
|
||||
@@ -21,6 +22,7 @@ public class UserPersistenceMapper {
|
||||
return UserEntity.builder()
|
||||
.id(user.id())
|
||||
.email(user.email())
|
||||
.name(user.name())
|
||||
.passwordHash(user.passwordHash())
|
||||
.role(UserEntity.RoleEnum.valueOf(user.role().name()))
|
||||
.createdAt(user.createdAt())
|
||||
|
||||
@@ -30,7 +30,7 @@ public class AuthController {
|
||||
@Operation(summary = "Créer un compte utilisateur")
|
||||
public UserResponse register(@Valid @RequestBody RegisterRequest request) {
|
||||
User user = registerUserUseCase.register(
|
||||
new RegisterUserUseCase.Command(request.email(), request.password())
|
||||
new RegisterUserUseCase.Command(request.email(), request.name(), request.password())
|
||||
);
|
||||
return userRestMapper.toResponse(user);
|
||||
}
|
||||
@@ -38,9 +38,10 @@ public class AuthController {
|
||||
@PostMapping("/login")
|
||||
@Operation(summary = "S'authentifier et obtenir un token JWT")
|
||||
public AuthResponse login(@Valid @RequestBody LoginRequest request) {
|
||||
String token = authenticateUserUseCase.authenticate(
|
||||
AuthenticateUserUseCase.Result result = authenticateUserUseCase.authenticate(
|
||||
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;
|
||||
|
||||
public record LoginRequest(
|
||||
@Email @NotBlank String email,
|
||||
@NotBlank String email,
|
||||
@NotBlank String password
|
||||
) {}
|
||||
|
||||
@@ -6,5 +6,6 @@ import jakarta.validation.constraints.Size;
|
||||
|
||||
public record RegisterRequest(
|
||||
@Email @NotBlank String email,
|
||||
@NotBlank String name,
|
||||
@NotBlank @Size(min = 8) String password
|
||||
) {}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
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.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(
|
||||
user.id(),
|
||||
user.email(),
|
||||
user.role().name(),
|
||||
user.name(),
|
||||
user.role().toApiRole(),
|
||||
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.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)
|
||||
@ActiveProfiles("test")
|
||||
@Testcontainers
|
||||
public abstract class AbstractIntegrationTest {
|
||||
|
||||
@Container
|
||||
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");
|
||||
}
|
||||
// Le container PostgreSQL est géré automatiquement via l'URL TC JDBC
|
||||
// définie dans application-test.yml : jdbc:tc:postgresql:16:///olhar_test
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class JwtServiceTest {
|
||||
|
||||
@Test
|
||||
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);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class AuthControllerIT extends AbstractIntegrationTest {
|
||||
|
||||
@Test
|
||||
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);
|
||||
|
||||
@@ -28,7 +28,7 @@ class AuthControllerIT extends AbstractIntegrationTest {
|
||||
|
||||
@Test
|
||||
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);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.postForEntity("/api/v1/auth/register", request, String.class);
|
||||
@@ -38,7 +38,7 @@ class AuthControllerIT extends AbstractIntegrationTest {
|
||||
|
||||
@Test
|
||||
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);
|
||||
|
||||
LoginRequest loginRequest = new LoginRequest("login@example.com", "password123");
|
||||
@@ -50,7 +50,7 @@ class AuthControllerIT extends AbstractIntegrationTest {
|
||||
|
||||
@Test
|
||||
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);
|
||||
|
||||
LoginRequest loginRequest = new LoginRequest("wrongpass@example.com", "wrongpassword");
|
||||
|
||||
Reference in New Issue
Block a user