Public Access
Added arch unit tests
This commit is contained in:
1 parent
7ba38fbbce
commit
c028e8d382
6 files changed
+151
-13
No files matched your search
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>4.1.0-SNAPSHOT</version>
|
||||
<version>4.1.0-M4</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>eu.andreasdahm.tbp</groupId>
|
||||
@@ -15,6 +15,8 @@
|
||||
<properties>
|
||||
<java.version>25</java.version>
|
||||
<jwt.version>0.13.0</jwt.version>
|
||||
<testcontainers.version>1.21.4</testcontainers.version>
|
||||
<archunit.version>1.4.1</archunit.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<!-- Spring -->
|
||||
@@ -128,6 +130,29 @@
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-testcontainers</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>test</scope>
|
||||
<version>${testcontainers.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${testcontainers.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.tngtech.archunit</groupId>
|
||||
<artifactId>archunit-junit5</artifactId>
|
||||
<version>${archunit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package auth_service.configuration.security;
|
||||
|
||||
import auth_service.domain.login.model.User;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -29,9 +28,9 @@ public class JwtService {
|
||||
*
|
||||
* @return A signed JWT string
|
||||
*/
|
||||
public String generateToken(User user) {
|
||||
public String generateToken(String email) {
|
||||
return Jwts.builder()
|
||||
.subject(user.getEmail())
|
||||
.subject(email)
|
||||
.issuedAt(new Date(System.currentTimeMillis()))
|
||||
.expiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION_MS))
|
||||
.signWith(getSigningKey())
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package auth_service.domain.login.controller;
|
||||
|
||||
import auth_service.configuration.security.JwtService;
|
||||
import auth_service.domain.login.controller.dto.request.LoginRequest;
|
||||
import auth_service.domain.login.controller.dto.response.LoginResponse;
|
||||
import auth_service.domain.login.controller.mapper.ApiMapper;
|
||||
@@ -21,6 +22,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
public class LoginController {
|
||||
|
||||
private final LoginService loginService;
|
||||
private final JwtService jwtService;
|
||||
|
||||
/**
|
||||
* Authenticates a user based on email and password.
|
||||
@@ -33,7 +35,9 @@ public class LoginController {
|
||||
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
|
||||
|
||||
var user = ApiMapper.toModel(request);
|
||||
var jwt = loginService.login(user);
|
||||
return ResponseEntity.ok(new LoginResponse("Login successful", jwt));
|
||||
var authenticatedUser = loginService.login(user);
|
||||
var token = jwtService.generateToken(authenticatedUser.getEmail());
|
||||
|
||||
return ResponseEntity.ok(new LoginResponse("Login successful", token));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package auth_service.domain.login.service;
|
||||
|
||||
import auth_service.configuration.security.JwtService;
|
||||
import auth_service.domain.login.exception.InvalidCredentialsException;
|
||||
import auth_service.domain.login.exception.UserNotFoundException;
|
||||
import auth_service.domain.login.model.User;
|
||||
@@ -16,15 +15,19 @@ import org.springframework.stereotype.Service;
|
||||
public class LoginService {
|
||||
private final MinimalUserRepository minimalUserRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtService jwtService;
|
||||
|
||||
public String login(User userToLogin) {
|
||||
/**
|
||||
* Authenticates a user based on email and raw password.
|
||||
*
|
||||
* @return The authenticated User model
|
||||
*/
|
||||
public User login(User userToLogin) {
|
||||
var userFromDatabase = minimalUserRepository.findByEmail(userToLogin.getEmail()).orElseThrow(() -> new UserNotFoundException("User not found with given email"));
|
||||
var loginSuccessful = passwordEncoder.matches(userToLogin.getPassword(), userFromDatabase.getPassword());
|
||||
if (!loginSuccessful) {
|
||||
throw new InvalidCredentialsException("Invalid credentials");
|
||||
}
|
||||
|
||||
return jwtService.generateToken(userToLogin);
|
||||
return userFromDatabase;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,29 @@
|
||||
package auth_service;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
@SpringBootTest
|
||||
@Disabled("Tests temporarily disabled until DB config is ready")
|
||||
@Testcontainers
|
||||
class AuthServiceApplicationTests {
|
||||
|
||||
/**
|
||||
* Define the PostgreSQL container.
|
||||
*
|
||||
* @ServiceConnection automatically configures the application properties
|
||||
* (URL, username, password) for this specific container.
|
||||
*/
|
||||
@Container
|
||||
@ServiceConnection
|
||||
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17.9");
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
// If the context fails to load (e.g. Flyway migration fails or beans are missing),
|
||||
// this test will fail automatically. No explicit assertions needed.
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package auth_service.architecture;
|
||||
|
||||
import com.tngtech.archunit.core.importer.ImportOption;
|
||||
import com.tngtech.archunit.junit.AnalyzeClasses;
|
||||
import com.tngtech.archunit.junit.ArchTest;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
|
||||
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
|
||||
|
||||
/**
|
||||
* ArchUnit tests to enforce Clean Architecture and package boundaries.
|
||||
* Analyzes all classes in the 'auth_service' package, ignoring test classes.
|
||||
*/
|
||||
@AnalyzeClasses(packages = "auth_service", importOptions = ImportOption.DoNotIncludeTests.class)
|
||||
class ArchitectureTest {
|
||||
|
||||
// ===================================================================================
|
||||
// 1. DOMAIN INDEPENDENCE (Slices)
|
||||
// ===================================================================================
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule domains_should_not_depend_on_each_other = SlicesRuleDefinition.slices()
|
||||
.matching("auth_service.domain.(*)..")
|
||||
.should().notDependOnEachOther()
|
||||
.because("Domains (e.g. login, user) should be strictly isolated and independent.");
|
||||
|
||||
// ===================================================================================
|
||||
// 2. CLEAN ARCHITECTURE DEPENDENCY DIRECTION (Inside out)
|
||||
// ===================================================================================
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule domain_core_should_not_depend_on_outside = noClasses()
|
||||
.that().resideInAnyPackage("..model..", "..service..")
|
||||
.should().dependOnClassesThat().resideInAnyPackage("..controller..", "..persistence.entities..", "..persistence.mapper..")
|
||||
.because("Domain core (Models, Services) must not depend on Web/Controller or Persistence details.");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule persistence_interfaces_belong_to_domain = classes()
|
||||
.that().resideInAPackage("..persistence..")
|
||||
.and().areInterfaces()
|
||||
.should().haveSimpleNameEndingWith("Repository")
|
||||
.because("Repository interfaces are the ports and belong to the domain/persistence boundary.");
|
||||
|
||||
// ===================================================================================
|
||||
// 3. ENCAPSULATION OF DTOs AND ENTITIES
|
||||
// ===================================================================================
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule dtos_should_only_be_used_in_controllers = classes()
|
||||
.that().resideInAPackage("..controller.dto..")
|
||||
.should().onlyBeAccessed().byAnyPackage("..controller..")
|
||||
.because("DTOs are Web/API concerns and must not leak into Services or Persistence.");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule entities_should_only_be_used_in_persistence = classes()
|
||||
.that().resideInAPackage("..persistence.entities..")
|
||||
.should().onlyBeAccessed().byAnyPackage("..persistence..")
|
||||
.because("Database Entities must not leak into the Domain Core or Controllers.");
|
||||
|
||||
// ===================================================================================
|
||||
// 4. ADDITIONAL BEST PRACTICES
|
||||
// ===================================================================================
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule no_cyclic_dependencies = SlicesRuleDefinition.slices()
|
||||
.matching("auth_service.(*)..")
|
||||
.should().beFreeOfCycles()
|
||||
.because("Cyclic dependencies make the code unmaintainable and tightly coupled.");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule controllers_should_only_call_services = noClasses()
|
||||
.that().resideInAPackage("..controller..")
|
||||
.should().dependOnClassesThat().resideInAPackage("..persistence..")
|
||||
.because("Controllers must delegate to Services and should not access Persistence directly.");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule services_should_be_annotated = classes()
|
||||
.that().resideInAPackage("..service..")
|
||||
.and().haveSimpleNameEndingWith("Service")
|
||||
.should().beAnnotatedWith(org.springframework.stereotype.Service.class)
|
||||
.because("Classes named *Service should be registered as Spring @Service beans.");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule controllers_should_be_annotated = classes()
|
||||
.that().resideInAPackage("..controller..")
|
||||
.and().haveSimpleNameEndingWith("Controller")
|
||||
.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.RestControllerAdvice.class)
|
||||
.because("Classes named *Controller should be registered as Spring @RestController or @ControllerAdvice or @RestControllerAdvice beans.");
|
||||
}
|
||||
Reference in new issue
Block a user