diff --git a/pom.xml b/pom.xml
index 521ef2a..5fffcca 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.boot
spring-boot-starter-parent
- 4.1.0-SNAPSHOT
+ 4.1.0-M4
eu.andreasdahm.tbp
@@ -15,6 +15,8 @@
25
0.13.0
+ 1.21.4
+ 1.4.1
@@ -128,6 +130,29 @@
spring-boot-devtools
runtime
+
+ org.springframework.boot
+ spring-boot-testcontainers
+ test
+
+
+ org.testcontainers
+ postgresql
+ test
+ ${testcontainers.version}
+
+
+ org.testcontainers
+ junit-jupiter
+ ${testcontainers.version}
+ test
+
+
+ com.tngtech.archunit
+ archunit-junit5
+ ${archunit.version}
+ test
+
diff --git a/src/main/java/auth_service/configuration/security/JwtService.java b/src/main/java/auth_service/configuration/security/JwtService.java
index 7cb0537..94479d9 100644
--- a/src/main/java/auth_service/configuration/security/JwtService.java
+++ b/src/main/java/auth_service/configuration/security/JwtService.java
@@ -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())
diff --git a/src/main/java/auth_service/domain/login/controller/LoginController.java b/src/main/java/auth_service/domain/login/controller/LoginController.java
index 33e41a7..3d5f5aa 100644
--- a/src/main/java/auth_service/domain/login/controller/LoginController.java
+++ b/src/main/java/auth_service/domain/login/controller/LoginController.java
@@ -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 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));
}
}
\ No newline at end of file
diff --git a/src/main/java/auth_service/domain/login/service/LoginService.java b/src/main/java/auth_service/domain/login/service/LoginService.java
index fecc628..ac80d6e 100644
--- a/src/main/java/auth_service/domain/login/service/LoginService.java
+++ b/src/main/java/auth_service/domain/login/service/LoginService.java
@@ -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;
}
}
\ No newline at end of file
diff --git a/src/test/java/auth_service/AuthServiceApplicationTests.java b/src/test/java/auth_service/AuthServiceApplicationTests.java
index 299a1cc..5baa49a 100644
--- a/src/test/java/auth_service/AuthServiceApplicationTests.java
+++ b/src/test/java/auth_service/AuthServiceApplicationTests.java
@@ -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.
}
-
}
\ No newline at end of file
diff --git a/src/test/java/auth_service/architecture/ArchitectureTest.java b/src/test/java/auth_service/architecture/ArchitectureTest.java
new file mode 100644
index 0000000..3db0dfc
--- /dev/null
+++ b/src/test/java/auth_service/architecture/ArchitectureTest.java
@@ -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.");
+}
\ No newline at end of file