Refactor package structure: domain -> modules

Move domain packages into a new modules.* hierarchy and rename controller packages to api.* across login, registration, and user modules. Update package declarations and imports (including shared.api DTOs and JwtService -> modules login TokenGenerator), adjust tests and persistence/mapper paths accordingly, and apply minor formatting fixes (missing EOF newlines). This reorganizes the project to better separate API surface and module boundaries.
This commit is contained in:
lobo committed 2026-04-19 12:50:01 +02:00
1 parent 3fcba271a6
commit 2a5839b736
73 files changed
+574 -569

No files matched your search

+4 -4
View File
@@ -19,20 +19,20 @@ The project strictly follows a Domain-Driven / Clean Architecture (Ports & Adapt
### Layer Breakdown ### Layer Breakdown
1. **Controller / Web Layer (`controller`)** 1. **API / Web Layer (`api`)**
* Contains REST endpoints, DTOs (`Request`/`Response` records), and Global Exception Handlers. * Contains REST endpoints, DTOs (`Request`/`Response` records), and Global Exception Handlers.
* Uses `ApiMapper` to map between DTOs and Domain Models. * Uses `ApiMapper` to map between DTOs and Domain Models.
* **Rule:** This layer delegates to the Service layer and never accesses persistence directly. * **Rule:** This layer delegates to the Service layer and never accesses persistence directly.
2. **Domain / Core Layer (`model`, `service`, `exception`)** 2. **Business / Core Layer (`model`, `service`, `exception`)**
* The heart of the application. Contains pure Java classes (Models) and Business Logic (Services). * The heart of the application. Contains pure Java classes (Models) and Business Logic (Services).
* Throws specific business exceptions (e.g., `InvalidCredentialsException`). * Throws specific business exceptions (e.g., `InvalidCredentialsException`).
* **Rule:** This layer has **no dependencies** on the Controller or Persistence layer. It defines interfaces that the persistence layer must implement. * **Rule:** This layer has **no dependencies** on the API or Persistence layer. It defines interfaces that the persistence layer must implement.
3. **Persistence Layer (`persistence`)** 3. **Persistence Layer (`persistence`)**
* Handles database operations. Contains JPA Entities (`UserEntity`), Spring Data Repositories, and the `EntityMapper`. * Handles database operations. Contains JPA Entities (`UserEntity`), Spring Data Repositories, and the `EntityMapper`.
* Implements the interfaces defined in the Domain layer. * Implements the interfaces defined in the Domain layer.
* **Rule:** Entities never leak into the Controller layer. * **Rule:** Entities never leak into the API layer.
*Note: Future implementations will include ArchUnit tests to automatically enforce these architectural boundaries during the CI pipeline.* *Note: Future implementations will include ArchUnit tests to automatically enforce these architectural boundaries during the CI pipeline.*
@@ -8,4 +8,4 @@ public class AuthServiceApplication {
static void main(String[] args) { static void main(String[] args) {
SpringApplication.run(AuthServiceApplication.class, args); SpringApplication.run(AuthServiceApplication.class, args);
} }
} }
@@ -7,4 +7,4 @@ import org.springframework.scheduling.annotation.EnableAsync;
@EnableAsync @EnableAsync
public class AsyncConfig { public class AsyncConfig {
// This class enables Spring's @Async annotation functionality globally // This class enables Spring's @Async annotation functionality globally
} }
@@ -34,4 +34,4 @@ public class CacheConfig {
.expireAfterAccess(10, TimeUnit.MINUTES) .expireAfterAccess(10, TimeUnit.MINUTES)
.recordStats(); .recordStats();
} }
} }
@@ -34,4 +34,4 @@ public class DevCacheConfig {
.expireAfterAccess(1, TimeUnit.SECONDS) .expireAfterAccess(1, TimeUnit.SECONDS)
.recordStats(); .recordStats();
} }
} }
@@ -21,4 +21,4 @@ public class ObservabilityConfig {
public TimedAspect timedAspect(MeterRegistry registry) { public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry); return new TimedAspect(registry);
} }
} }
@@ -65,4 +65,4 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }
} }
@@ -12,4 +12,4 @@ import org.springframework.context.annotation.Configuration;
public class JwtConfig { public class JwtConfig {
private String secret; private String secret;
} }
@@ -1,6 +1,6 @@
package auth_service.configuration.security; package auth_service.configuration.security;
import auth_service.domain.login.service.TokenGenerator; import auth_service.modules.login.service.TokenGenerator;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -77,4 +77,4 @@ public class JwtService implements TokenGenerator {
byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8); byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes); return Keys.hmacShaKeyFor(keyBytes);
} }
} }
@@ -91,4 +91,4 @@ public class SecurityConfig {
return http.build(); return http.build();
} }
} }
@@ -1,9 +1,9 @@
package auth_service.domain.login.controller; package auth_service.modules.login.api;
import auth_service.domain.login.controller.dto.request.LoginRequest; import auth_service.modules.login.api.dto.request.LoginRequest;
import auth_service.domain.login.controller.dto.response.LoginResponse; import auth_service.modules.login.api.dto.response.LoginResponse;
import auth_service.domain.login.controller.mapper.ApiMapper; import auth_service.modules.login.api.mapper.ApiMapper;
import auth_service.domain.login.service.LoginService; import auth_service.modules.login.service.LoginService;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
/** /**
* Controller handling authentication requests. * API handling authentication requests.
*/ */
@RestController @RestController
@RequestMapping("/api/auth") @RequestMapping("/api/auth")
@@ -36,4 +36,4 @@ public class LoginController {
var token = loginService.login(user); var token = loginService.login(user);
return ResponseEntity.ok(new LoginResponse("Login successful", token)); return ResponseEntity.ok(new LoginResponse("Login successful", token));
} }
} }
@@ -1,8 +1,8 @@
package auth_service.domain.login.controller; package auth_service.modules.login.api;
import auth_service.shared.controller.dto.response.ErrorResponse; import auth_service.shared.api.dto.response.ErrorResponse;
import auth_service.domain.login.exception.InvalidCredentialsException; import auth_service.modules.login.exception.InvalidCredentialsException;
import auth_service.domain.login.exception.UserNotFoundException; import auth_service.modules.login.exception.UserNotFoundException;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
@@ -39,4 +39,4 @@ public class LoginExceptionController {
var body = new ErrorResponse("INVALID_CREDENTIALS", ex.getMessage()); var body = new ErrorResponse("INVALID_CREDENTIALS", ex.getMessage());
return response(body, HttpStatus.UNAUTHORIZED); return response(body, HttpStatus.UNAUTHORIZED);
} }
} }
@@ -1,4 +1,4 @@
package auth_service.domain.login.controller.dto.request; package auth_service.modules.login.api.dto.request;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
@@ -7,4 +7,4 @@ import jakarta.validation.constraints.NotNull;
* DTO (Data Transfer Object) representing the incoming login request. * DTO (Data Transfer Object) representing the incoming login request.
*/ */
public record LoginRequest(@NotNull String email, @NotNull @NotBlank String password) { public record LoginRequest(@NotNull String email, @NotNull @NotBlank String password) {
} }
@@ -1,8 +1,8 @@
package auth_service.domain.login.controller.dto.response; package auth_service.modules.login.api.dto.response;
/** /**
* DTO representing the outgoing login response. * DTO representing the outgoing login response.
* The email will be null if the login fails. * The email will be null if the login fails.
*/ */
public record LoginResponse(String message, String token) { public record LoginResponse(String message, String token) {
} }
@@ -1,7 +1,7 @@
package auth_service.domain.login.controller.mapper; package auth_service.modules.login.api.mapper;
import auth_service.domain.login.controller.dto.request.LoginRequest; import auth_service.modules.login.api.dto.request.LoginRequest;
import auth_service.domain.login.model.User; import auth_service.modules.login.model.User;
public abstract class ApiMapper { public abstract class ApiMapper {
private ApiMapper() { private ApiMapper() {
@@ -10,4 +10,4 @@ public abstract class ApiMapper {
public static User toModel(LoginRequest loginRequest) { public static User toModel(LoginRequest loginRequest) {
return User.builder().email(loginRequest.email()).password(loginRequest.password()).build(); return User.builder().email(loginRequest.email()).password(loginRequest.password()).build();
} }
} }
@@ -1,7 +1,7 @@
package auth_service.domain.login.exception; package auth_service.modules.login.exception;
public class InvalidCredentialsException extends RuntimeException { public class InvalidCredentialsException extends RuntimeException {
public InvalidCredentialsException(String message) { public InvalidCredentialsException(String message) {
super(message); super(message);
} }
} }
@@ -1,7 +1,7 @@
package auth_service.domain.login.exception; package auth_service.modules.login.exception;
public class UserNotFoundException extends RuntimeException { public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) { public UserNotFoundException(String message) {
super(message); super(message);
} }
} }
@@ -1,4 +1,4 @@
package auth_service.domain.login.model; package auth_service.modules.login.model;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@@ -9,4 +9,4 @@ public class User {
private long id; private long id;
private String email; private String email;
private String password; private String password;
} }
@@ -1,6 +1,6 @@
package auth_service.domain.login.persistence; package auth_service.modules.login.persistence;
import auth_service.domain.login.model.User; import auth_service.modules.login.model.User;
import java.util.Optional; import java.util.Optional;
@@ -17,4 +17,4 @@ public interface MinimalUserRepository {
Optional<User> findByEmail(String email); Optional<User> findByEmail(String email);
void updateLoginDate(long userId); void updateLoginDate(long userId);
} }
@@ -1,8 +1,8 @@
package auth_service.domain.login.persistence; package auth_service.modules.login.persistence;
import auth_service.domain.login.model.User; import auth_service.modules.login.model.User;
import auth_service.domain.login.persistence.entities.MinimalUserEntity; import auth_service.modules.login.persistence.entities.MinimalUserEntity;
import auth_service.domain.login.persistence.mapper.EntityMapper; import auth_service.modules.login.persistence.mapper.EntityMapper;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery; import jakarta.persistence.TypedQuery;
@@ -44,4 +44,4 @@ public class MinimalUserRepositoryImpl implements MinimalUserRepository {
entityManager.flush(); entityManager.flush();
} }
} }
} }
@@ -1,4 +1,4 @@
package auth_service.domain.login.persistence.entities; package auth_service.modules.login.persistence.entities;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.*; import lombok.*;
@@ -32,4 +32,4 @@ public class MinimalUserEntity {
@Column(name = "last_login_date") @Column(name = "last_login_date")
private LocalDateTime lastLoginDate; private LocalDateTime lastLoginDate;
} }
@@ -1,7 +1,7 @@
package auth_service.domain.login.persistence.mapper; package auth_service.modules.login.persistence.mapper;
import auth_service.domain.login.model.User; import auth_service.modules.login.model.User;
import auth_service.domain.login.persistence.entities.MinimalUserEntity; import auth_service.modules.login.persistence.entities.MinimalUserEntity;
public abstract class EntityMapper { public abstract class EntityMapper {
@@ -19,4 +19,4 @@ public abstract class EntityMapper {
return model; return model;
} }
} }
@@ -1,9 +1,9 @@
package auth_service.domain.login.service; package auth_service.modules.login.service;
import auth_service.domain.login.exception.InvalidCredentialsException; import auth_service.modules.login.exception.InvalidCredentialsException;
import auth_service.domain.login.exception.UserNotFoundException; import auth_service.modules.login.exception.UserNotFoundException;
import auth_service.domain.login.model.User; import auth_service.modules.login.model.User;
import auth_service.domain.login.persistence.MinimalUserRepository; import auth_service.modules.login.persistence.MinimalUserRepository;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
@@ -32,4 +32,4 @@ public class LoginService {
minimalUserRepository.updateLoginDate(foundUser.getId()); minimalUserRepository.updateLoginDate(foundUser.getId());
return tokenGenerator.generateToken(String.valueOf(foundUser.getId()), foundUser.getEmail()); return tokenGenerator.generateToken(String.valueOf(foundUser.getId()), foundUser.getEmail());
} }
} }
@@ -1,4 +1,4 @@
package auth_service.domain.login.service; package auth_service.modules.login.service;
/** /**
* Port interface for token generation. * Port interface for token generation.
@@ -14,3 +14,4 @@ public interface TokenGenerator {
*/ */
String generateToken(String userId, String email); String generateToken(String userId, String email);
} }
@@ -1,9 +1,9 @@
package auth_service.domain.registration.controller; package auth_service.modules.registration.api;
import auth_service.domain.registration.controller.dto.mapper.ApiMapper; import auth_service.modules.registration.api.dto.mapper.ApiMapper;
import auth_service.domain.registration.controller.dto.request.RegistrationRequest; import auth_service.modules.registration.api.dto.request.RegistrationRequest;
import auth_service.domain.registration.controller.dto.response.RegistrationResponse; import auth_service.modules.registration.api.dto.response.RegistrationResponse;
import auth_service.domain.registration.service.RegistrationService; import auth_service.modules.registration.service.RegistrationService;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -25,4 +25,4 @@ public class RegistrationController {
var userId = registrationService.register(ApiMapper.mapToModel(request)); var userId = registrationService.register(ApiMapper.mapToModel(request));
return ResponseEntity.ok(RegistrationResponse.builder().userId(userId).build()); return ResponseEntity.ok(RegistrationResponse.builder().userId(userId).build());
} }
} }
@@ -1,9 +1,9 @@
package auth_service.domain.registration.controller; package auth_service.modules.registration.api;
import auth_service.shared.controller.dto.response.ErrorResponse; import auth_service.shared.api.dto.response.ErrorResponse;
import auth_service.domain.registration.exception.EmailAlreadyInUseException; import auth_service.modules.registration.exception.EmailAlreadyInUseException;
import auth_service.domain.registration.exception.InvalidInvitationCodeException; import auth_service.modules.registration.exception.InvalidInvitationCodeException;
import auth_service.domain.registration.exception.SteamIdAlreadyRegisteredException; import auth_service.modules.registration.exception.SteamIdAlreadyRegisteredException;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
@@ -42,4 +42,4 @@ public class RegistrationExceptionController {
} }
} }
@@ -1,7 +1,7 @@
package auth_service.domain.registration.controller.dto.mapper; package auth_service.modules.registration.api.dto.mapper;
import auth_service.domain.registration.controller.dto.request.RegistrationRequest; import auth_service.modules.registration.api.dto.request.RegistrationRequest;
import auth_service.domain.registration.model.Registration; import auth_service.modules.registration.model.Registration;
public abstract class ApiMapper { public abstract class ApiMapper {
private ApiMapper() { private ApiMapper() {
@@ -16,4 +16,4 @@ public abstract class ApiMapper {
build(); build();
} }
} }
@@ -1,7 +1,7 @@
package auth_service.domain.registration.controller.dto.request; package auth_service.modules.registration.api.dto.request;
import lombok.Builder; import lombok.Builder;
@Builder @Builder
public record RegistrationRequest(String email, String password, String inviteCode, long steamId) { public record RegistrationRequest(String email, String password, String inviteCode, long steamId) {
} }
@@ -1,7 +1,7 @@
package auth_service.domain.registration.controller.dto.response; package auth_service.modules.registration.api.dto.response;
import lombok.Builder; import lombok.Builder;
@Builder @Builder
public record RegistrationResponse(long userId) { public record RegistrationResponse(long userId) {
} }
@@ -1,7 +1,7 @@
package auth_service.domain.registration.exception; package auth_service.modules.registration.exception;
public class EmailAlreadyInUseException extends RuntimeException { public class EmailAlreadyInUseException extends RuntimeException {
public EmailAlreadyInUseException(String message) { public EmailAlreadyInUseException(String message) {
super(message); super(message);
} }
} }
@@ -1,7 +1,7 @@
package auth_service.domain.registration.exception; package auth_service.modules.registration.exception;
public class InvalidInvitationCodeException extends RuntimeException { public class InvalidInvitationCodeException extends RuntimeException {
public InvalidInvitationCodeException(String message) { public InvalidInvitationCodeException(String message) {
super(message); super(message);
} }
} }
@@ -1,7 +1,7 @@
package auth_service.domain.registration.exception; package auth_service.modules.registration.exception;
public class SteamIdAlreadyRegisteredException extends RuntimeException { public class SteamIdAlreadyRegisteredException extends RuntimeException {
public SteamIdAlreadyRegisteredException(String message) { public SteamIdAlreadyRegisteredException(String message) {
super(message); super(message);
} }
} }
@@ -1,4 +1,4 @@
package auth_service.domain.registration.model; package auth_service.modules.registration.model;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@@ -11,4 +11,4 @@ public class Registration {
private String encryptedPassword; private String encryptedPassword;
private String inviteCode; private String inviteCode;
private long steamId; private long steamId;
} }
@@ -1,6 +1,6 @@
package auth_service.domain.registration.persistence; package auth_service.modules.registration.persistence;
import auth_service.domain.registration.model.Registration; import auth_service.modules.registration.model.Registration;
public interface RegisteredUserRepository { public interface RegisteredUserRepository {
boolean existsByEmail(String email); boolean existsByEmail(String email);
@@ -8,4 +8,4 @@ public interface RegisteredUserRepository {
boolean existsBySteamId(long steamId); boolean existsBySteamId(long steamId);
long save(Registration registration); long save(Registration registration);
} }
@@ -1,7 +1,7 @@
package auth_service.domain.registration.persistence; package auth_service.modules.registration.persistence;
import auth_service.domain.registration.model.Registration; import auth_service.modules.registration.model.Registration;
import auth_service.domain.registration.persistence.mapper.EntityMapper; import auth_service.modules.registration.persistence.mapper.EntityMapper;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
@@ -40,4 +40,4 @@ public class RegisteredUserRepositoryImpl implements RegisteredUserRepository {
entityManager.persist(userToRegisterEntity); entityManager.persist(userToRegisterEntity);
return userToRegisterEntity.getId(); return userToRegisterEntity.getId();
} }
} }
@@ -1,4 +1,4 @@
package auth_service.domain.registration.persistence.entities; package auth_service.modules.registration.persistence.entities;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.*; import lombok.*;
@@ -34,4 +34,4 @@ public class RegisteredUserEntity {
@Column(name = "last_login_date") @Column(name = "last_login_date")
private LocalDateTime lastLoginDate; private LocalDateTime lastLoginDate;
} }
@@ -1,7 +1,7 @@
package auth_service.domain.registration.persistence.mapper; package auth_service.modules.registration.persistence.mapper;
import auth_service.domain.registration.model.Registration; import auth_service.modules.registration.model.Registration;
import auth_service.domain.registration.persistence.entities.RegisteredUserEntity; import auth_service.modules.registration.persistence.entities.RegisteredUserEntity;
import java.time.Clock; import java.time.Clock;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -21,4 +21,4 @@ public abstract class EntityMapper {
lastLoginDate(null). lastLoginDate(null).
build(); build();
} }
} }
@@ -1,4 +1,4 @@
package auth_service.domain.registration.service; package auth_service.modules.registration.service;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -16,4 +16,4 @@ public class CodeValidationService {
return code.equals(invitationCode); return code.equals(invitationCode);
} }
} }
@@ -1,10 +1,10 @@
package auth_service.domain.registration.service; package auth_service.modules.registration.service;
import auth_service.domain.registration.exception.EmailAlreadyInUseException; import auth_service.modules.registration.exception.EmailAlreadyInUseException;
import auth_service.domain.registration.exception.InvalidInvitationCodeException; import auth_service.modules.registration.exception.InvalidInvitationCodeException;
import auth_service.domain.registration.exception.SteamIdAlreadyRegisteredException; import auth_service.modules.registration.exception.SteamIdAlreadyRegisteredException;
import auth_service.domain.registration.model.Registration; import auth_service.modules.registration.model.Registration;
import auth_service.domain.registration.persistence.RegisteredUserRepository; import auth_service.modules.registration.persistence.RegisteredUserRepository;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -35,4 +35,4 @@ public class RegistrationService {
return registeredUserRepository.save(registration); return registeredUserRepository.save(registration);
} }
} }
@@ -1,10 +1,10 @@
package auth_service.domain.user.controller; package auth_service.modules.user.api;
import auth_service.shared.security.UserPrincipal; import auth_service.shared.security.UserPrincipal;
import auth_service.domain.user.controller.dto.request.UserSettingsRequest; import auth_service.modules.user.api.dto.request.UserSettingsRequest;
import auth_service.domain.user.controller.dto.response.UserProfileResponse; import auth_service.modules.user.api.dto.response.UserProfileResponse;
import auth_service.domain.user.controller.mapper.ApiMapper; import auth_service.modules.user.api.mapper.ApiMapper;
import auth_service.domain.user.service.UserService; import auth_service.modules.user.service.UserService;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -12,7 +12,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
/** /**
* Controller handling user profile operations for the dashboard. * API handling user profile operations for the dashboard.
*/ */
@RestController @RestController
@RequestMapping("/api/users") @RequestMapping("/api/users")
@@ -42,4 +42,4 @@ public class UserController {
this.userService.updateUserSettings(principal.getIdAsLong(), request.steamId(), ApiMapper.toDomain(request)); this.userService.updateUserSettings(principal.getIdAsLong(), request.steamId(), ApiMapper.toDomain(request));
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
} }
@@ -1,7 +1,7 @@
package auth_service.domain.user.controller; package auth_service.modules.user.api;
import auth_service.shared.controller.dto.response.ErrorResponse; import auth_service.shared.api.dto.response.ErrorResponse;
import auth_service.domain.user.exception.UserNotFoundException; import auth_service.modules.user.exception.UserNotFoundException;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
@@ -33,4 +33,4 @@ public class UserExceptionController {
return response(body, HttpStatus.NOT_FOUND); return response(body, HttpStatus.NOT_FOUND);
} }
} }
@@ -1,4 +1,4 @@
package auth_service.domain.user.controller.dto.request; package auth_service.modules.user.api.dto.request;
public record UserSettingsRequest(boolean useDarkTheme, String language, long steamId) { public record UserSettingsRequest(boolean useDarkTheme, String language, long steamId) {
} }
@@ -1,7 +1,7 @@
package auth_service.domain.user.controller.dto.response; package auth_service.modules.user.api.dto.response;
import lombok.Builder; import lombok.Builder;
@Builder @Builder
public record SettingsResponse(long id, boolean useDarkTheme, String language) { public record SettingsResponse(long id, boolean useDarkTheme, String language) {
} }
@@ -1,4 +1,4 @@
package auth_service.domain.user.controller.dto.response; package auth_service.modules.user.api.dto.response;
import lombok.Builder; import lombok.Builder;
@@ -6,4 +6,4 @@ import java.time.LocalDateTime;
@Builder @Builder
public record UserProfileResponse(long id, String email, String role, LocalDateTime lastLogin, LocalDateTime createdDate, long steamId, SettingsResponse settings) { public record UserProfileResponse(long id, String email, String role, LocalDateTime lastLogin, LocalDateTime createdDate, long steamId, SettingsResponse settings) {
} }
@@ -1,11 +1,11 @@
package auth_service.domain.user.controller.mapper; package auth_service.modules.user.api.mapper;
import auth_service.domain.user.controller.dto.request.UserSettingsRequest; import auth_service.modules.user.api.dto.request.UserSettingsRequest;
import auth_service.domain.user.controller.dto.response.SettingsResponse; import auth_service.modules.user.api.dto.response.SettingsResponse;
import auth_service.domain.user.controller.dto.response.UserProfileResponse; import auth_service.modules.user.api.dto.response.UserProfileResponse;
import auth_service.domain.user.model.User; import auth_service.modules.user.model.User;
import auth_service.domain.user.model.UserSettings; import auth_service.modules.user.model.UserSettings;
public abstract class ApiMapper { public abstract class ApiMapper {
private ApiMapper() { private ApiMapper() {
@@ -34,4 +34,4 @@ public abstract class ApiMapper {
.build(); .build();
} }
} }
@@ -1,7 +1,7 @@
package auth_service.domain.user.exception; package auth_service.modules.user.exception;
public class UserNotFoundException extends RuntimeException { public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) { public UserNotFoundException(String message) {
super(message); super(message);
} }
} }
@@ -1,4 +1,4 @@
package auth_service.domain.user.model; package auth_service.modules.user.model;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@@ -16,4 +16,4 @@ public class User {
private long steamId; private long steamId;
private UserSettings settings; private UserSettings settings;
} }
@@ -1,4 +1,4 @@
package auth_service.domain.user.model; package auth_service.modules.user.model;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@@ -8,4 +8,4 @@ import lombok.Data;
public class UserSettings { public class UserSettings {
private String language; private String language;
private String theme; private String theme;
} }
@@ -1,8 +1,8 @@
package auth_service.domain.user.persistence; package auth_service.modules.user.persistence;
import auth_service.domain.user.model.User; import auth_service.modules.user.model.User;
import auth_service.domain.user.model.UserSettings; import auth_service.modules.user.model.UserSettings;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
@@ -25,4 +25,4 @@ public interface UserRepository {
@Transactional @Transactional
boolean updateSettings(long userId, long steamId, UserSettings settings); boolean updateSettings(long userId, long steamId, UserSettings settings);
} }
@@ -1,10 +1,10 @@
package auth_service.domain.user.persistence; package auth_service.modules.user.persistence;
import auth_service.domain.user.model.User; import auth_service.modules.user.model.User;
import auth_service.domain.user.model.UserSettings; import auth_service.modules.user.model.UserSettings;
import auth_service.domain.user.persistence.entities.UserEntity; import auth_service.modules.user.persistence.entities.UserEntity;
import auth_service.domain.user.persistence.entities.UserSettingsEntity; import auth_service.modules.user.persistence.entities.UserSettingsEntity;
import auth_service.domain.user.persistence.mapper.EntityMapper; import auth_service.modules.user.persistence.mapper.EntityMapper;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery; import jakarta.persistence.TypedQuery;
@@ -81,4 +81,4 @@ public class UserRepositoryImpl implements UserRepository {
return steamIdChanged; return steamIdChanged;
} }
} }
@@ -1,4 +1,4 @@
package auth_service.domain.user.persistence.entities; package auth_service.modules.user.persistence.entities;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.*; import lombok.*;
@@ -42,4 +42,4 @@ public class UserEntity {
@OneToOne(cascade = jakarta.persistence.CascadeType.ALL, fetch = jakarta.persistence.FetchType.LAZY) @OneToOne(cascade = jakarta.persistence.CascadeType.ALL, fetch = jakarta.persistence.FetchType.LAZY)
@jakarta.persistence.PrimaryKeyJoinColumn @jakarta.persistence.PrimaryKeyJoinColumn
private UserSettingsEntity settings; private UserSettingsEntity settings;
} }
@@ -1,4 +1,4 @@
package auth_service.domain.user.persistence.entities; package auth_service.modules.user.persistence.entities;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
@@ -23,4 +23,4 @@ public class UserSettingsEntity {
@Column(nullable = false) @Column(nullable = false)
private String theme; private String theme;
} }
@@ -1,9 +1,9 @@
package auth_service.domain.user.persistence.mapper; package auth_service.modules.user.persistence.mapper;
import auth_service.domain.user.model.User; import auth_service.modules.user.model.User;
import auth_service.domain.user.model.UserSettings; import auth_service.modules.user.model.UserSettings;
import auth_service.domain.user.persistence.entities.UserEntity; import auth_service.modules.user.persistence.entities.UserEntity;
import auth_service.domain.user.persistence.entities.UserSettingsEntity; import auth_service.modules.user.persistence.entities.UserSettingsEntity;
public abstract class EntityMapper { public abstract class EntityMapper {
@@ -49,4 +49,4 @@ public abstract class EntityMapper {
entity.setTheme(settings.getTheme()); entity.setTheme(settings.getTheme());
return entity; return entity;
} }
} }
@@ -1,9 +1,9 @@
package auth_service.domain.user.service; package auth_service.modules.user.service;
import auth_service.domain.user.exception.UserNotFoundException; import auth_service.modules.user.exception.UserNotFoundException;
import auth_service.domain.user.model.User; import auth_service.modules.user.model.User;
import auth_service.domain.user.model.UserSettings; import auth_service.modules.user.model.UserSettings;
import auth_service.domain.user.persistence.UserRepository; import auth_service.modules.user.persistence.UserRepository;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
@@ -38,4 +38,4 @@ public class UserService {
} }
log.info("User settings has been updated!"); log.info("User settings has been updated!");
} }
} }
@@ -1,12 +1,13 @@
package auth_service.shared.controller.dto.response; package auth_service.shared.api.dto.response;
import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NonNull;
/** /**
* Shared error response record for all domains. * Shared error response record for all modules.
* *
* @param code A machine-readable error code. * @param code A machine-readable error code.
* @param message A human-readable error message. * @param message A human-readable error message.
*/ */
public record ErrorResponse(@NonNull String code, @NonNull String message) { public record ErrorResponse(@NonNull String code, @NonNull String message) {
} }
@@ -15,4 +15,4 @@ public record UserPrincipal(
public long getIdAsLong() { public long getIdAsLong() {
return Long.parseLong(id); return Long.parseLong(id);
} }
} }
@@ -10,4 +10,4 @@ class AuthServiceApplicationTests extends AbstractIntegrationTest {
// If the context fails to load (e.g. Flyway migration fails or beans are missing), // If the context fails to load (e.g. Flyway migration fails or beans are missing),
// this test will fail automatically. No explicit assertions needed. // this test will fail automatically. No explicit assertions needed.
} }
} }
@@ -11,31 +11,31 @@ import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
/** /**
* ArchUnit tests to enforce Clean Architecture and package boundaries. * ArchUnit tests to enforce Hexagonal Architecture and package boundaries.
* Analyzes all classes in the 'auth_service' package, ignoring test classes. * Analyzes all classes in the 'auth_service' package, ignoring test classes.
*/ */
@AnalyzeClasses(packages = "auth_service", importOptions = ImportOption.DoNotIncludeTests.class) @AnalyzeClasses(packages = "auth_service", importOptions = ImportOption.DoNotIncludeTests.class)
class ArchitectureTest extends AbstractArchitectureTest { class ArchitectureTest extends AbstractArchitectureTest {
// =================================================================================== // ===================================================================================
// 1. DOMAIN INDEPENDENCE (Slices) // 1. MODULE INDEPENDENCE (Slices)
// =================================================================================== // ===================================================================================
@ArchTest @ArchTest
static final ArchRule domains_should_not_depend_on_each_other = SlicesRuleDefinition.slices() static final ArchRule modules_should_not_depend_on_each_other = SlicesRuleDefinition.slices()
.matching("auth_service.domain.(*)..") .matching("auth_service.modules.(*)..")
.should().notDependOnEachOther() .should().notDependOnEachOther()
.because("Domains (e.g. login, user) should be strictly isolated and independent."); .because("Modules (e.g. login, user) should be strictly isolated and independent.");
// =================================================================================== // ===================================================================================
// 2. CLEAN ARCHITECTURE DEPENDENCY DIRECTION (Inside out) // 2. HEXAGONAL ARCHITECTURE DEPENDENCY DIRECTION (Inside out)
// =================================================================================== // ===================================================================================
@ArchTest @ArchTest
static final ArchRule domain_core_should_not_depend_on_outside = noClasses() static final ArchRule domain_core_should_not_depend_on_outside = noClasses()
.that().resideInAnyPackage("..model..", "..service..") .that().resideInAnyPackage("..model..", "..service..")
.should().dependOnClassesThat().resideInAnyPackage("..controller..", "..persistence.entities..", "..persistence.mapper..") .should().dependOnClassesThat().resideInAnyPackage("..api..", "..persistence.entities..", "..persistence.mapper..")
.because("Domain core (Models, Services) must not depend on Web/Controller or Persistence details."); .because("Domain core (Models, Services) must not depend on API or Persistence details.");
@ArchTest @ArchTest
static final ArchRule persistence_interfaces_belong_to_domain = classes() static final ArchRule persistence_interfaces_belong_to_domain = classes()
@@ -49,16 +49,16 @@ class ArchitectureTest extends AbstractArchitectureTest {
// =================================================================================== // ===================================================================================
@ArchTest @ArchTest
static final ArchRule dtos_should_only_be_used_in_controllers = classes() static final ArchRule dtos_should_only_be_used_in_api = classes()
.that().resideInAPackage("..controller.dto..") .that().resideInAPackage("..api.dto..")
.should().onlyBeAccessed().byAnyPackage("..controller..") .should().onlyBeAccessed().byAnyPackage("..api..")
.because("DTOs are Web/API concerns and must not leak into Services or Persistence."); .because("DTOs are API concerns and must not leak into Services or Persistence.");
@ArchTest @ArchTest
static final ArchRule entities_should_only_be_used_in_persistence = classes() static final ArchRule entities_should_only_be_used_in_persistence = classes()
.that().resideInAPackage("..persistence.entities..") .that().resideInAPackage("..persistence.entities..")
.should().onlyBeAccessed().byAnyPackage("..persistence..") .should().onlyBeAccessed().byAnyPackage("..persistence..")
.because("Database Entities must not leak into the Domain Core or Controllers."); .because("Database Entities must not leak into the Domain Core or APIs.");
// =================================================================================== // ===================================================================================
// 4. ADDITIONAL BEST PRACTICES // 4. ADDITIONAL BEST PRACTICES
@@ -71,28 +71,28 @@ class ArchitectureTest extends AbstractArchitectureTest {
.because("Cyclic dependencies make the code unmaintainable and tightly coupled."); .because("Cyclic dependencies make the code unmaintainable and tightly coupled.");
@ArchTest @ArchTest
static final ArchRule controllers_should_only_call_services = noClasses() static final ArchRule apis_should_only_call_services = noClasses()
.that().resideInAPackage("..controller..") .that().resideInAPackage("..api..")
.should().dependOnClassesThat().resideInAPackage("..persistence..") .should().dependOnClassesThat().resideInAPackage("..persistence..")
.because("Controllers must delegate to Services and should not access Persistence directly."); .because("APIs must delegate to Services and should not access Persistence directly.");
@ArchTest @ArchTest
static final ArchRule persistence_should_not_depend_on_controllers = noClasses() static final ArchRule persistence_should_not_depend_on_apis = noClasses()
.that().resideInAPackage("..persistence..") .that().resideInAPackage("..persistence..")
.should().dependOnClassesThat().resideInAPackage("..controller..") .should().dependOnClassesThat().resideInAPackage("..api..")
.because("Persistence layer must be independent of the Web/Controller layer."); .because("Persistence layer must be independent of the API layer.");
@ArchTest @ArchTest
static final ArchRule exceptions_should_be_isolated = noClasses() static final ArchRule exceptions_should_be_isolated = noClasses()
.that().resideInAPackage("..exception..") .that().resideInAPackage("..exception..")
.should().dependOnClassesThat().resideInAnyPackage("..controller..", "..persistence.entities..") .should().dependOnClassesThat().resideInAnyPackage("..api..", "..persistence.entities..")
.because("Exceptions should be part of the domain core and independent of infrastructure details."); .because("Exceptions should be part of the domain core and independent of infrastructure details.");
@ArchTest @ArchTest
static final ArchRule configuration_should_not_be_accessed_by_domain = noClasses() static final ArchRule configuration_should_not_be_accessed_by_modules = noClasses()
.that().resideInAnyPackage("..domain..") .that().resideInAnyPackage("..modules..")
.should().dependOnClassesThat().resideInAPackage("auth_service.configuration..") .should().dependOnClassesThat().resideInAPackage("auth_service.configuration..")
.because("Domain logic should not depend on Spring configuration classes."); .because("Module logic should not depend on global Spring configuration classes.");
@ArchTest @ArchTest
static final ArchRule services_should_be_annotated = classes() static final ArchRule services_should_be_annotated = classes()
@@ -106,17 +106,17 @@ class ArchitectureTest extends AbstractArchitectureTest {
// =================================================================================== // ===================================================================================
@ArchTest @ArchTest
static final ArchRule shared_should_not_depend_on_domains = noClasses() static final ArchRule shared_should_not_depend_on_modules = noClasses()
.that().resideInAPackage("auth_service.shared..") .that().resideInAPackage("auth_service.shared..")
.should().dependOnClassesThat().resideInAPackage("auth_service.domain..") .should().dependOnClassesThat().resideInAPackage("auth_service.modules..")
.because("Shared classes should be independent of specific domains."); .because("Shared classes should be independent of specific modules.");
@ArchTest @ArchTest
static final ArchRule controllers_should_be_annotated = classes() static final ArchRule apis_should_be_annotated = classes()
.that().resideInAPackage("..controller..") .that().resideInAPackage("..api..")
.and().haveSimpleNameEndingWith("Controller") .and().haveSimpleNameEndingWith("Controller")
.should().beAnnotatedWith(org.springframework.web.bind.annotation.RestController.class) .should().beAnnotatedWith(org.springframework.web.bind.annotation.RestController.class)
.orShould().beAnnotatedWith(org.springframework.web.bind.annotation.ControllerAdvice.class) .orShould().beAnnotatedWith(org.springframework.web.bind.annotation.ControllerAdvice.class)
.orShould().beAnnotatedWith(org.springframework.web.bind.annotation.RestControllerAdvice.class) .orShould().beAnnotatedWith(org.springframework.web.bind.annotation.RestControllerAdvice.class)
.because("Classes named *Controller should be registered as Spring @RestController or @ControllerAdvice or @RestControllerAdvice beans."); .because("Classes in 'api' package should be registered as Spring @RestController or @ControllerAdvice or @RestControllerAdvice beans.");
} }
@@ -91,4 +91,4 @@ class JwtServiceTest extends AbstractUnitTest {
// Assert // Assert
assertFalse(isValid); assertFalse(isValid);
} }
} }
@@ -1,72 +1,72 @@
package auth_service.domain.login.controller; package auth_service.modules.login.api;
import auth_service.configuration.security.JwtAuthenticationFilter; import auth_service.configuration.security.JwtAuthenticationFilter;
import auth_service.domain.login.controller.dto.request.LoginRequest; import auth_service.modules.login.api.dto.request.LoginRequest;
import auth_service.domain.login.exception.InvalidCredentialsException; import auth_service.modules.login.exception.InvalidCredentialsException;
import auth_service.domain.login.model.User; import auth_service.modules.login.model.User;
import auth_service.domain.login.service.LoginService; import auth_service.modules.login.service.LoginService;
import auth_service.shared.test.AbstractWebMvcTest; import auth_service.shared.test.AbstractWebMvcTest;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.AutoConfigureJsonTesters; import org.springframework.boot.test.autoconfigure.json.AutoConfigureJsonTesters;
import org.springframework.boot.test.json.JacksonTester; import org.springframework.boot.test.json.JacksonTester;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/** /**
* Tests for LoginController. * Tests for LoginController.
*/ */
@WebMvcTest(LoginController.class) @WebMvcTest(LoginController.class)
@AutoConfigureMockMvc(addFilters = false) // Skip security filters for simple controller test @AutoConfigureMockMvc(addFilters = false) // Skip security filters for simple controller test
@AutoConfigureJsonTesters @AutoConfigureJsonTesters
class LoginControllerTest extends AbstractWebMvcTest { class LoginControllerTest extends AbstractWebMvcTest {
@Autowired @Autowired
private MockMvc mockMvc; private MockMvc mockMvc;
@MockitoBean @MockitoBean
private LoginService loginService; private LoginService loginService;
@MockitoBean @MockitoBean
private JwtAuthenticationFilter jwtAuthenticationFilter; // Needed because it's used in SecurityConfig private JwtAuthenticationFilter jwtAuthenticationFilter; // Needed because it's used in SecurityConfig
@Autowired @Autowired
private JacksonTester<LoginRequest> loginRequestJson; private JacksonTester<LoginRequest> loginRequestJson;
@Test @Test
void login_Success() throws Exception { void login_Success() throws Exception {
// Arrange // Arrange
var request = new LoginRequest("test@example.com", "password"); var request = new LoginRequest("test@example.com", "password");
when(loginService.login(any(User.class))).thenReturn("mock-token"); when(loginService.login(any(User.class))).thenReturn("mock-token");
// Act & Assert // Act & Assert
mockMvc.perform(post("/api/auth/login") mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(loginRequestJson.write(request).getJson())) .content(loginRequestJson.write(request).getJson()))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("Login successful")) .andExpect(jsonPath("$.message").value("Login successful"))
.andExpect(jsonPath("$.token").value("mock-token")); .andExpect(jsonPath("$.token").value("mock-token"));
} }
@Test @Test
void login_Failure() throws Exception { void login_Failure() throws Exception {
// Arrange // Arrange
var request = new LoginRequest("test@example.com", "wrong-password"); var request = new LoginRequest("test@example.com", "wrong-password");
when(loginService.login(any(User.class))).thenThrow(new InvalidCredentialsException("Invalid credentials")); when(loginService.login(any(User.class))).thenThrow(new InvalidCredentialsException("Invalid credentials"));
// Act & Assert // Act & Assert
mockMvc.perform(post("/api/auth/login") mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(loginRequestJson.write(request).getJson())) .content(loginRequestJson.write(request).getJson()))
.andExpect(status().isUnauthorized()); .andExpect(status().isUnauthorized());
} }
} }
@@ -1,6 +1,6 @@
package auth_service.domain.login.persistence; package auth_service.modules.login.persistence;
import auth_service.domain.login.persistence.entities.MinimalUserEntity; import auth_service.modules.login.persistence.entities.MinimalUserEntity;
import auth_service.shared.test.AbstractPersistenceTest; import auth_service.shared.test.AbstractPersistenceTest;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -57,4 +57,4 @@ class MinimalUserRepositoryTest extends AbstractPersistenceTest {
var updated = entityManager.find(MinimalUserEntity.class, entity.getId()); var updated = entityManager.find(MinimalUserEntity.class, entity.getId());
assertNotNull(updated.getLastLoginDate()); assertNotNull(updated.getLastLoginDate());
} }
} }
@@ -1,88 +1,88 @@
package auth_service.domain.login.service; package auth_service.modules.login.service;
import auth_service.domain.login.exception.InvalidCredentialsException; import auth_service.modules.login.exception.InvalidCredentialsException;
import auth_service.domain.login.exception.UserNotFoundException; import auth_service.modules.login.exception.UserNotFoundException;
import auth_service.domain.login.model.User; import auth_service.modules.login.model.User;
import auth_service.domain.login.persistence.MinimalUserRepository; import auth_service.modules.login.persistence.MinimalUserRepository;
import auth_service.shared.test.AbstractUnitTest; import auth_service.shared.test.AbstractUnitTest;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Optional; import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
/** /**
* Tests for LoginService. * Tests for LoginService.
*/ */
class LoginServiceTest extends AbstractUnitTest { class LoginServiceTest extends AbstractUnitTest {
@Mock @Mock
private MinimalUserRepository minimalUserRepository; private MinimalUserRepository minimalUserRepository;
@Mock @Mock
private PasswordEncoder passwordEncoder; private PasswordEncoder passwordEncoder;
@Mock @Mock
private TokenGenerator tokenGenerator; private TokenGenerator tokenGenerator;
@InjectMocks @InjectMocks
private LoginService loginService; private LoginService loginService;
private User user; private User user;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
user = User.builder() user = User.builder()
.email("test@example.com") .email("test@example.com")
.password("password") .password("password")
.build(); .build();
} }
@Test @Test
void login_Success() { void login_Success() {
// Arrange // Arrange
var dbUser = User.builder() var dbUser = User.builder()
.id(815L) .id(815L)
.email("test@example.com") .email("test@example.com")
.password("encodedPassword") .password("encodedPassword")
.build(); .build();
when(minimalUserRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(dbUser)); when(minimalUserRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(dbUser));
when(passwordEncoder.matches(user.getPassword(), dbUser.getPassword())).thenReturn(true); when(passwordEncoder.matches(user.getPassword(), dbUser.getPassword())).thenReturn(true);
when(tokenGenerator.generateToken("815", "test@example.com")).thenReturn("mock-token"); when(tokenGenerator.generateToken("815", "test@example.com")).thenReturn("mock-token");
// Act // Act
var result = loginService.login(user); var result = loginService.login(user);
// Assert // Assert
assertEquals("mock-token", result); assertEquals("mock-token", result);
} }
@Test @Test
void login_UserNotFound() { void login_UserNotFound() {
// Arrange // Arrange
when(minimalUserRepository.findByEmail(user.getEmail())).thenReturn(Optional.empty()); when(minimalUserRepository.findByEmail(user.getEmail())).thenReturn(Optional.empty());
// Act & Assert // Act & Assert
assertThrows(UserNotFoundException.class, () -> loginService.login(user)); assertThrows(UserNotFoundException.class, () -> loginService.login(user));
} }
@Test @Test
void login_InvalidCredentials() { void login_InvalidCredentials() {
// Arrange // Arrange
var dbUser = User.builder() var dbUser = User.builder()
.email("test@example.com") .email("test@example.com")
.password("encodedPassword") .password("encodedPassword")
.build(); .build();
when(minimalUserRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(dbUser)); when(minimalUserRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(dbUser));
when(passwordEncoder.matches(user.getPassword(), dbUser.getPassword())).thenReturn(false); when(passwordEncoder.matches(user.getPassword(), dbUser.getPassword())).thenReturn(false);
// Act & Assert // Act & Assert
assertThrows(InvalidCredentialsException.class, () -> loginService.login(user)); assertThrows(InvalidCredentialsException.class, () -> loginService.login(user));
} }
} }
@@ -1,11 +1,11 @@
package auth_service.domain.registration.controller; package auth_service.modules.registration.api;
import auth_service.configuration.security.JwtAuthenticationFilter; import auth_service.configuration.security.JwtAuthenticationFilter;
import auth_service.configuration.security.JwtService; import auth_service.configuration.security.JwtService;
import auth_service.domain.registration.controller.dto.request.RegistrationRequest; import auth_service.modules.registration.api.dto.request.RegistrationRequest;
import auth_service.domain.registration.exception.EmailAlreadyInUseException; import auth_service.modules.registration.exception.EmailAlreadyInUseException;
import auth_service.domain.registration.model.Registration; import auth_service.modules.registration.model.Registration;
import auth_service.domain.registration.service.RegistrationService; import auth_service.modules.registration.service.RegistrationService;
import auth_service.shared.test.AbstractWebMvcTest; import auth_service.shared.test.AbstractWebMvcTest;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -91,4 +91,4 @@ class RegistrationControllerTest extends AbstractWebMvcTest {
.content(registrationRequestJson.write(request).getJson())) .content(registrationRequestJson.write(request).getJson()))
.andExpect(status().isBadRequest()); // Adjust status based on your @ExceptionHandler .andExpect(status().isBadRequest()); // Adjust status based on your @ExceptionHandler
} }
} }
@@ -1,7 +1,7 @@
package auth_service.domain.registration.persistence; package auth_service.modules.registration.persistence;
import auth_service.domain.registration.model.Registration; import auth_service.modules.registration.model.Registration;
import auth_service.domain.registration.persistence.entities.RegisteredUserEntity; import auth_service.modules.registration.persistence.entities.RegisteredUserEntity;
import auth_service.shared.test.AbstractPersistenceTest; import auth_service.shared.test.AbstractPersistenceTest;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -75,4 +75,4 @@ class RegisteredUserRepositoryTest extends AbstractPersistenceTest {
assertNotNull(saved); assertNotNull(saved);
assertEquals("save@example.com", saved.getEmail()); assertEquals("save@example.com", saved.getEmail());
} }
} }
@@ -1,10 +1,10 @@
package auth_service.domain.registration.service; package auth_service.modules.registration.service;
import auth_service.domain.registration.exception.EmailAlreadyInUseException; import auth_service.modules.registration.exception.EmailAlreadyInUseException;
import auth_service.domain.registration.exception.InvalidInvitationCodeException; import auth_service.modules.registration.exception.InvalidInvitationCodeException;
import auth_service.domain.registration.exception.SteamIdAlreadyRegisteredException; import auth_service.modules.registration.exception.SteamIdAlreadyRegisteredException;
import auth_service.domain.registration.model.Registration; import auth_service.modules.registration.model.Registration;
import auth_service.domain.registration.persistence.RegisteredUserRepository; import auth_service.modules.registration.persistence.RegisteredUserRepository;
import auth_service.shared.test.AbstractUnitTest; import auth_service.shared.test.AbstractUnitTest;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -103,4 +103,4 @@ class RegistrationServiceTest extends AbstractUnitTest {
verify(registeredUserRepository, never()).save(any()); verify(registeredUserRepository, never()).save(any());
} }
} }
@@ -1,113 +1,113 @@
package auth_service.domain.user.controller; package auth_service.modules.user.api;
import auth_service.configuration.security.JwtAuthenticationFilter; import auth_service.configuration.security.JwtAuthenticationFilter;
import auth_service.configuration.security.JwtService; import auth_service.configuration.security.JwtService;
import auth_service.shared.security.UserPrincipal; import auth_service.shared.security.UserPrincipal;
import auth_service.domain.user.controller.dto.request.UserSettingsRequest; import auth_service.modules.user.api.dto.request.UserSettingsRequest;
import auth_service.domain.user.exception.UserNotFoundException; import auth_service.modules.user.exception.UserNotFoundException;
import auth_service.domain.user.model.User; import auth_service.modules.user.model.User;
import auth_service.domain.user.model.UserSettings; import auth_service.modules.user.model.UserSettings;
import auth_service.domain.user.service.UserService; import auth_service.modules.user.service.UserService;
import auth_service.shared.test.AbstractWebMvcTest; import auth_service.shared.test.AbstractWebMvcTest;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.AutoConfigureJsonTesters; import org.springframework.boot.test.autoconfigure.json.AutoConfigureJsonTesters;
import org.springframework.boot.test.json.JacksonTester; import org.springframework.boot.test.json.JacksonTester;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import java.util.Collections; import java.util.Collections;
import static org.mockito.ArgumentMatchers.*; import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(UserController.class) @WebMvcTest(UserController.class)
@AutoConfigureMockMvc(addFilters = false) @AutoConfigureMockMvc(addFilters = false)
@AutoConfigureJsonTesters @AutoConfigureJsonTesters
class UserControllerTest extends AbstractWebMvcTest { class UserControllerTest extends AbstractWebMvcTest {
@Autowired @Autowired
private MockMvc mockMvc; private MockMvc mockMvc;
@MockitoBean @MockitoBean
private UserService userService; private UserService userService;
@MockitoBean @MockitoBean
private JwtService jwtService; private JwtService jwtService;
@MockitoBean @MockitoBean
private JwtAuthenticationFilter jwtAuthenticationFilter; private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired @Autowired
private JacksonTester<UserSettingsRequest> userSettingsRequestJson; private JacksonTester<UserSettingsRequest> userSettingsRequestJson;
@AfterEach @AfterEach
void tearDown() { void tearDown() {
SecurityContextHolder.clearContext(); SecurityContextHolder.clearContext();
} }
@Test @Test
void getCurrentUserV1_Success() throws Exception { void getCurrentUserV1_Success() throws Exception {
// Arrange // Arrange
var email = "test@example.com"; var email = "test@example.com";
var settings = UserSettings.builder().language("en").theme("dark").build(); var settings = UserSettings.builder().language("en").theme("dark").build();
var user = User.builder().email(email).settings(settings).build(); var user = User.builder().email(email).settings(settings).build();
var principal = new UserPrincipal("1", email); var principal = new UserPrincipal("1", email);
SecurityContextHolder.getContext().setAuthentication( SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList()) new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
); );
when(userService.getUserByEmail(email)).thenReturn(user); when(userService.getUserByEmail(email)).thenReturn(user);
// Act & Assert // Act & Assert
mockMvc.perform(get("/api/users/me") mockMvc.perform(get("/api/users/me")
.accept(MediaType.APPLICATION_JSON)) .accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.email").value(email)); .andExpect(jsonPath("$.email").value(email));
} }
@Test @Test
void getCurrentUserV1_NotFound() throws Exception { void getCurrentUserV1_NotFound() throws Exception {
// Arrange // Arrange
var email = "notfound@example.com"; var email = "notfound@example.com";
var principal = new UserPrincipal("1", email); var principal = new UserPrincipal("1", email);
SecurityContextHolder.getContext().setAuthentication( SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList()) new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
); );
when(userService.getUserByEmail(email)).thenThrow(new UserNotFoundException("User not found")); when(userService.getUserByEmail(email)).thenThrow(new UserNotFoundException("User not found"));
// Act & Assert // Act & Assert
mockMvc.perform(get("/api/users/me") mockMvc.perform(get("/api/users/me")
.accept(MediaType.APPLICATION_JSON)) .accept(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound()); .andExpect(status().isNotFound());
} }
@Test @Test
void updateUserSettings_Success() throws Exception { void updateUserSettings_Success() throws Exception {
// Arrange // Arrange
var email = "test@example.com"; var email = "test@example.com";
var principal = new UserPrincipal("1", email); var principal = new UserPrincipal("1", email);
SecurityContextHolder.getContext().setAuthentication( SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList()) new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
); );
var request = new UserSettingsRequest(true, "en", 123456789L); var request = new UserSettingsRequest(true, "en", 123456789L);
// Act & Assert // Act & Assert
mockMvc.perform(put("/api/users/settings") mockMvc.perform(put("/api/users/settings")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(userSettingsRequestJson.write(request).getJson())) .content(userSettingsRequestJson.write(request).getJson()))
.andExpect(status().isOk()); .andExpect(status().isOk());
verify(userService).updateUserSettings(eq(1L), eq(123456789L), any()); verify(userService).updateUserSettings(eq(1L), eq(123456789L), any());
} }
} }
@@ -1,7 +1,7 @@
package auth_service.domain.user.persistence; package auth_service.modules.user.persistence;
import auth_service.domain.user.model.UserSettings; import auth_service.modules.user.model.UserSettings;
import auth_service.domain.user.persistence.entities.UserEntity; import auth_service.modules.user.persistence.entities.UserEntity;
import auth_service.shared.test.AbstractPersistenceTest; import auth_service.shared.test.AbstractPersistenceTest;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -73,4 +73,4 @@ class UserRepositoryTest extends AbstractPersistenceTest {
assertNotNull(updatedUser.getSettings()); assertNotNull(updatedUser.getSettings());
assertEquals("de", updatedUser.getSettings().getLanguage()); assertEquals("de", updatedUser.getSettings().getLanguage());
} }
} }
@@ -1,67 +1,67 @@
package auth_service.domain.user.service; package auth_service.modules.user.service;
import auth_service.domain.user.exception.UserNotFoundException; import auth_service.modules.user.exception.UserNotFoundException;
import auth_service.domain.user.model.User; import auth_service.modules.user.model.User;
import auth_service.domain.user.model.UserSettings; import auth_service.modules.user.model.UserSettings;
import auth_service.domain.user.persistence.UserRepository; import auth_service.modules.user.persistence.UserRepository;
import auth_service.shared.test.AbstractUnitTest; import auth_service.shared.test.AbstractUnitTest;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import java.util.Optional; import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
/** /**
* Tests for UserService. * Tests for UserService.
*/ */
class UserServiceTest extends AbstractUnitTest { class UserServiceTest extends AbstractUnitTest {
@Mock @Mock
private UserRepository userRepository; private UserRepository userRepository;
@InjectMocks @InjectMocks
private UserService userService; private UserService userService;
@Test @Test
void getUserByEmail_Success() { void getUserByEmail_Success() {
// Arrange // Arrange
var email = "test@example.com"; var email = "test@example.com";
var user = User.builder().email(email).build(); var user = User.builder().email(email).build();
when(userRepository.findByEmail(email)).thenReturn(Optional.of(user)); when(userRepository.findByEmail(email)).thenReturn(Optional.of(user));
// Act // Act
var result = userService.getUserByEmail(email); var result = userService.getUserByEmail(email);
// Assert // Assert
assertEquals(user, result); assertEquals(user, result);
} }
@Test @Test
void getUserByEmail_NotFound() { void getUserByEmail_NotFound() {
// Arrange // Arrange
var email = "notfound@example.com"; var email = "notfound@example.com";
when(userRepository.findByEmail(email)).thenReturn(Optional.empty()); when(userRepository.findByEmail(email)).thenReturn(Optional.empty());
// Act & Assert // Act & Assert
assertThrows(UserNotFoundException.class, () -> userService.getUserByEmail(email)); assertThrows(UserNotFoundException.class, () -> userService.getUserByEmail(email));
} }
@Test @Test
void updateUserSettings_Success() { void updateUserSettings_Success() {
// Arrange // Arrange
long userId = 1L; long userId = 1L;
long steamId = 123456789L; long steamId = 123456789L;
var userSettings = UserSettings.builder().language("en").build(); var userSettings = UserSettings.builder().language("en").build();
when(userRepository.updateSettings(userId, steamId, userSettings)).thenReturn(true); when(userRepository.updateSettings(userId, steamId, userSettings)).thenReturn(true);
// Act // Act
userService.updateUserSettings(userId, steamId, userSettings); userService.updateUserSettings(userId, steamId, userSettings);
// Assert // Assert
verify(userRepository, times(1)).updateSettings(userId, steamId, userSettings); verify(userRepository, times(1)).updateSettings(userId, steamId, userSettings);
} }
} }
@@ -8,3 +8,4 @@ import org.junit.jupiter.api.Tag;
@Tag("ArchitectureTest") @Tag("ArchitectureTest")
public abstract class AbstractArchitectureTest { public abstract class AbstractArchitectureTest {
} }
@@ -22,3 +22,4 @@ public abstract class AbstractIntegrationTest {
@ServiceConnection @ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17-alpine"); static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17-alpine");
} }
@@ -21,4 +21,4 @@ public abstract class AbstractPersistenceTest {
@Container @Container
@ServiceConnection @ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17-alpine"); static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17-alpine");
} }
@@ -12,3 +12,4 @@ import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
public abstract class AbstractUnitTest { public abstract class AbstractUnitTest {
} }
@@ -10,4 +10,4 @@ import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
@Tag("WebMvcTest") @Tag("WebMvcTest")
@WebMvcTest @WebMvcTest
public abstract class AbstractWebMvcTest { public abstract class AbstractWebMvcTest {
} }