diff --git a/MIGRATION.md b/MIGRATION.md
new file mode 100644
index 0000000..5ba128f
--- /dev/null
+++ b/MIGRATION.md
@@ -0,0 +1,387 @@
+# Migration Guide
+
+This guide covers migrating applications using the Spring User Framework between major versions.
+
+## Table of Contents
+
+- [Migration Guide](#migration-guide)
+ - [Table of Contents](#table-of-contents)
+ - [Migrating to 4.0.x (Spring Boot 4.0)](#migrating-to-40x-spring-boot-40)
+ - [Prerequisites](#prerequisites)
+ - [Step 1: Update Java Version](#step-1-update-java-version)
+ - [Step 2: Update Dependencies](#step-2-update-dependencies)
+ - [Step 3: Spring Security 7 Changes](#step-3-spring-security-7-changes)
+ - [URL Pattern Requirements](#url-pattern-requirements)
+ - [Security Configuration Changes](#security-configuration-changes)
+ - [Step 4: Update Test Infrastructure](#step-4-update-test-infrastructure)
+ - [Test Dependency Changes](#test-dependency-changes)
+ - [Test Annotation Import Changes](#test-annotation-import-changes)
+ - [Step 5: Jackson 3 Changes](#step-5-jackson-3-changes)
+ - [Step 6: API Changes](#step-6-api-changes)
+ - [Profile Update Endpoint](#profile-update-endpoint)
+ - [Step 7: Configuration Changes](#step-7-configuration-changes)
+ - [For Developers Extending the Framework](#for-developers-extending-the-framework)
+ - [Extending Security Configuration](#extending-security-configuration)
+ - [Custom User Services](#custom-user-services)
+ - [Custom Controllers](#custom-controllers)
+ - [Event Listeners](#event-listeners)
+ - [Troubleshooting](#troubleshooting)
+ - [Common Issues](#common-issues)
+ - [Version Compatibility Matrix](#version-compatibility-matrix)
+
+## Migrating to 4.0.x (Spring Boot 4.0)
+
+This section covers migrating from Spring User Framework 3.x (Spring Boot 3.x) to 4.x (Spring Boot 4.0).
+
+### Prerequisites
+
+Before starting the migration:
+
+1. Ensure your application is running on the latest 3.5.x version
+2. Review your custom security configurations
+3. Audit any code that extends framework classes
+4. Back up your database (schema changes are minimal but recommended)
+
+### Step 1: Update Java Version
+
+**Spring Boot 4.0 requires Java 21 or higher.**
+
+Update your build configuration:
+
+**Gradle:**
+```groovy
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(21)
+ }
+}
+```
+
+**Maven:**
+```xml
+
+ 21
+
+```
+
+Ensure your CI/CD pipelines and deployment environments support Java 21.
+
+### Step 2: Update Dependencies
+
+Update the framework dependency version:
+
+**Gradle:**
+```groovy
+implementation 'com.digitalsanctuary:ds-spring-user-framework:4.0.0'
+```
+
+**Maven:**
+```xml
+
+ com.digitalsanctuary
+ ds-spring-user-framework
+ 4.0.0
+
+```
+
+Update Spring Boot:
+```groovy
+plugins {
+ id 'org.springframework.boot' version '4.0.0'
+}
+```
+
+### Step 3: Spring Security 7 Changes
+
+Spring Boot 4.0 includes Spring Security 7, which has breaking changes from Spring Security 6.x.
+
+#### URL Pattern Requirements
+
+**All URL patterns must now start with `/`.**
+
+This affects:
+- `user.security.unprotectedURIs` configuration
+- `user.security.protectedURIs` configuration
+- Any custom security matchers in your code
+
+**Before (3.x):**
+```yaml
+user:
+ security:
+ unprotectedURIs: /,/index.html,/css/**,/js/**,error,error.html
+```
+
+**After (4.x):**
+```yaml
+user:
+ security:
+ unprotectedURIs: /,/index.html,/css/**,/js/**,/error,/error.html
+```
+
+Note the `/error` and `/error.html` now have leading slashes.
+
+#### Security Configuration Changes
+
+If you have custom security configuration extending or working with the framework:
+
+**Deprecated methods removed:**
+- `authorizeRequests()` → use `authorizeHttpRequests()`
+- `antMatchers()` → use `requestMatchers()`
+- `mvcMatchers()` → use `requestMatchers()`
+
+**Example migration:**
+
+```java
+// Before (3.x)
+http.authorizeRequests()
+ .antMatchers("/public/**").permitAll()
+ .anyRequest().authenticated();
+
+// After (4.x)
+http.authorizeHttpRequests(authz -> authz
+ .requestMatchers("/public/**").permitAll()
+ .anyRequest().authenticated());
+```
+
+### Step 4: Update Test Infrastructure
+
+Spring Boot 4.0 introduces modular test packages.
+
+#### Test Dependency Changes
+
+Add the new modular test starters:
+
+**Gradle:**
+```groovy
+testImplementation 'org.springframework.boot:spring-boot-starter-test'
+testImplementation 'org.springframework.boot:spring-boot-data-jpa-test'
+testImplementation 'org.springframework.boot:spring-boot-webmvc-test'
+testImplementation 'org.springframework.boot:spring-boot-starter-security-test'
+testImplementation 'org.springframework.security:spring-security-test'
+```
+
+**Maven:**
+```xml
+
+ org.springframework.boot
+ spring-boot-data-jpa-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-webmvc-test
+ test
+
+```
+
+#### Test Annotation Import Changes
+
+Update imports for test annotations:
+
+| Annotation | Old Package (3.x) | New Package (4.x) |
+|------------|-------------------|-------------------|
+| `@AutoConfigureMockMvc` | `org.springframework.boot.test.autoconfigure.web.servlet` | `org.springframework.boot.webmvc.test.autoconfigure` |
+| `@WebMvcTest` | `org.springframework.boot.test.autoconfigure.web.servlet` | `org.springframework.boot.webmvc.test.autoconfigure` |
+| `@DataJpaTest` | `org.springframework.boot.test.autoconfigure.orm.jpa` | `org.springframework.boot.data.jpa.test.autoconfigure` |
+| `@AutoConfigureTestDatabase` | `org.springframework.boot.test.autoconfigure.jdbc` | `org.springframework.boot.jdbc.test.autoconfigure` |
+
+**Example:**
+```java
+// Before (3.x)
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+
+// After (4.x)
+import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
+import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
+```
+
+### Step 5: Jackson 3 Changes
+
+Spring Boot 4.0 uses Jackson 3.x for JSON processing.
+
+**ObjectMapper instantiation:**
+```java
+// Before (Jackson 2.x)
+ObjectMapper mapper = new ObjectMapper();
+
+// After (Jackson 3.x)
+ObjectMapper mapper = JsonMapper.builder().build();
+```
+
+**Package changes:**
+- Some classes moved from `com.fasterxml.jackson` to new packages
+- Check any custom serializers/deserializers
+
+### Step 6: API Changes
+
+#### Profile Update Endpoint
+
+The `/user/updateUser` endpoint now accepts `UserProfileUpdateDto` instead of `UserDto`.
+
+**Before (3.x):**
+```json
+POST /user/updateUser
+{
+ "email": "user@example.com",
+ "firstName": "John",
+ "lastName": "Doe",
+ "password": "...",
+ "matchingPassword": "..."
+}
+```
+
+**After (4.x):**
+```json
+POST /user/updateUser
+{
+ "firstName": "John",
+ "lastName": "Doe"
+}
+```
+
+This change improves security by not requiring password fields for profile updates.
+
+**Update your frontend code** if you're calling this endpoint directly.
+
+### Step 7: Configuration Changes
+
+Review your `application.yml` for any deprecated properties:
+
+| Deprecated Property | Replacement |
+|---------------------|-------------|
+| (none currently) | - |
+
+Most configuration properties remain unchanged between 3.x and 4.x.
+
+## For Developers Extending the Framework
+
+If you've extended framework classes or implemented custom functionality, review these sections carefully.
+
+### Extending Security Configuration
+
+If you have a custom `WebSecurityConfig` or extend the framework's security configuration:
+
+1. **Ensure all URL patterns start with `/`**
+2. **Update to lambda DSL style** (required in Spring Security 7)
+3. **Review method security annotations** - `@PreAuthorize`, `@PostAuthorize` unchanged
+
+**Example custom security configuration:**
+```java
+@Configuration
+@EnableWebSecurity
+public class CustomSecurityConfig {
+
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ http
+ .authorizeHttpRequests(authz -> authz
+ // All patterns must start with /
+ .requestMatchers("/api/public/**").permitAll()
+ .requestMatchers("/api/admin/**").hasRole("ADMIN")
+ .anyRequest().authenticated()
+ )
+ .formLogin(form -> form
+ .loginPage("/user/login.html")
+ .permitAll()
+ );
+ return http.build();
+ }
+}
+```
+
+### Custom User Services
+
+If you extend `UserService` or implement custom user management:
+
+1. **Method signatures unchanged** - Core service methods remain compatible
+2. **Password encoding** - Still uses BCrypt, no changes required
+3. **User entity** - No schema changes required
+
+### Custom Controllers
+
+If you have controllers that extend or work alongside framework controllers:
+
+1. **DTOs** - Update any code using `UserDto` for profile updates to use `UserProfileUpdateDto`
+2. **Validation** - Bean validation works the same way
+3. **Response format** - `JSONResponse` unchanged
+
+### Event Listeners
+
+Event handling remains unchanged:
+
+- `OnRegistrationCompleteEvent`
+- `UserPreDeleteEvent`
+- `AuditEvent`
+
+All events fire as before with the same payload structures.
+
+## Troubleshooting
+
+### Common Issues
+
+**Issue: `pattern must start with a /`**
+
+This error occurs when URL patterns in security configuration don't start with `/`.
+
+**Solution:** Review all entries in:
+- `user.security.unprotectedURIs`
+- `user.security.protectedURIs`
+- Any custom `requestMatchers()` calls
+
+Ensure every pattern starts with `/`.
+
+---
+
+**Issue: `ClassNotFoundException` for test annotations**
+
+Spring Boot 4.0 moved test annotations to new packages.
+
+**Solution:**
+1. Add the modular test dependencies (see [Step 4](#step-4-update-test-infrastructure))
+2. Update imports to new package locations
+
+---
+
+**Issue: `NoClassDefFoundError: com/fasterxml/jackson/...`**
+
+Jackson 3 has different package structures.
+
+**Solution:** Update ObjectMapper instantiation and check custom serializers.
+
+---
+
+**Issue: Profile update returns validation error for password**
+
+The `/user/updateUser` endpoint now uses `UserProfileUpdateDto`.
+
+**Solution:** Update your frontend to only send `firstName` and `lastName` fields.
+
+---
+
+**Issue: Java version incompatibility**
+
+Spring Boot 4.0 requires Java 21.
+
+**Solution:**
+1. Update your JDK to 21+
+2. Update build configuration
+3. Update CI/CD pipelines
+4. Update deployment environments
+
+## Version Compatibility Matrix
+
+| Framework Version | Spring Boot | Spring Security | Java | Status |
+|-------------------|-------------|-----------------|------|--------|
+| 4.0.x | 4.0.x | 7.x | 21+ | Current |
+| 3.5.x | 3.5.x | 6.x | 17+ | Maintained |
+| 3.4.x | 3.4.x | 6.x | 17+ | Security fixes only |
+| < 3.4 | < 3.4 | < 6 | 17+ | End of life |
+
+---
+
+For additional help, see:
+- [README](README.md) - Main documentation
+- [Configuration Guide](CONFIG.md) - All configuration options
+- [Demo Application](https://github.com/devondragon/SpringUserFrameworkDemoApp) - Working example
+- [GitHub Issues](https://github.com/devondragon/SpringUserFramework/issues) - Report problems
diff --git a/README.md b/README.md
index 91860d8..8db3bcb 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,9 @@
[](https://central.sonatype.com/artifact/com.digitalsanctuary/ds-spring-user-framework)
[](https://opensource.org/licenses/Apache-2.0)
-[](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
+[](https://spring.io/projects/spring-boot)
+[](https://spring.io/projects/spring-boot)
+[](https://www.oracle.com/java/technologies/downloads/)
A comprehensive Spring Boot User Management Framework that simplifies the implementation of robust user authentication and management features. Built on top of Spring Security, this library provides ready-to-use solutions for user registration, login, account management, and more.
@@ -15,8 +17,9 @@ Check out the [Spring User Framework Demo Application](https://github.com/devond
- [Table of Contents](#table-of-contents)
- [Features](#features)
- [Installation](#installation)
- - [Maven](#maven)
- - [Gradle](#gradle)
+ - [Spring Boot 4.0 (Latest)](#spring-boot-40-latest)
+ - [Spring Boot 3.5 (Stable)](#spring-boot-35-stable)
+ - [Migration Guide](#migration-guide)
- [Quick Start](#quick-start)
- [Prerequisites](#prerequisites)
- [Step 1: Add Dependencies](#step-1-add-dependencies)
@@ -90,49 +93,105 @@ Check out the [Spring User Framework Demo Application](https://github.com/devond
## Installation
-### Maven
+Choose the version that matches your Spring Boot version:
+| Spring Boot Version | Framework Version | Java Version | Spring Security |
+|---------------------|-------------------|--------------|-----------------|
+| 4.0.x | 4.0.x | 21+ | 7.x |
+| 3.5.x | 3.5.x | 17+ | 6.x |
+
+### Spring Boot 4.0 (Latest)
+
+Spring Boot 4.0 brings significant changes including Spring Security 7 and requires Java 21.
+
+**Maven:**
```xml
com.digitalsanctuary
ds-spring-user-framework
- 3.5.1
+ 4.0.0
```
-### Gradle
+**Gradle:**
+```groovy
+implementation 'com.digitalsanctuary:ds-spring-user-framework:4.0.0'
+```
+
+#### Spring Boot 4.0 Key Changes
+
+When upgrading to Spring Boot 4.0, be aware of these important changes:
+
+- **Java 21 Required**: Spring Boot 4.0 requires Java 21 or higher
+- **Spring Security 7**: Includes breaking changes from Spring Security 6.x
+ - All URL patterns in security configuration must start with `/`
+ - Some deprecated APIs have been removed
+- **Jackson 3**: JSON processing uses Jackson 3.x with some API changes
+- **Modular Test Infrastructure**: Test annotations have moved to new packages:
+ - `@AutoConfigureMockMvc` → `org.springframework.boot.webmvc.test.autoconfigure`
+ - `@DataJpaTest` → `org.springframework.boot.data.jpa.test.autoconfigure`
+ - `@WebMvcTest` → `org.springframework.boot.webmvc.test.autoconfigure`
+
+For testing, you may need these additional dependencies:
+```groovy
+testImplementation 'org.springframework.boot:spring-boot-data-jpa-test'
+testImplementation 'org.springframework.boot:spring-boot-webmvc-test'
+testImplementation 'org.springframework.boot:spring-boot-starter-security-test'
+```
+
+**Upgrading from 3.x?** See the [Migration Guide](MIGRATION.md) for detailed upgrade instructions.
+
+### Spring Boot 3.5 (Stable)
+
+For projects using Spring Boot 3.5.x with Java 17+:
+
+**Maven:**
+```xml
+
+ com.digitalsanctuary
+ ds-spring-user-framework
+ 3.5.1
+
+```
+**Gradle:**
```groovy
implementation 'com.digitalsanctuary:ds-spring-user-framework:3.5.1'
```
+## Migration Guide
+
+If you're upgrading from a previous version, see the **[Migration Guide](MIGRATION.md)** for:
+
+- Step-by-step upgrade instructions
+- Breaking changes and how to address them
+- Spring Security 7 compatibility requirements
+- Test infrastructure changes
+- Guidance for developers extending the framework
+
## Quick Start
Follow these steps to get up and running with the Spring User Framework in your application.
### Prerequisites
-- Java 17 or higher
-- Spring Boot 3.0+
+- **For Spring Boot 4.0**: Java 21 or higher
+- **For Spring Boot 3.5**: Java 17 or higher
- A database (MariaDB, PostgreSQL, MySQL, H2, etc.)
- SMTP server for email functionality (optional but recommended)
### Step 1: Add Dependencies
-1. **Add the main framework dependency**:
+1. **Add the main framework dependency** (see [Installation](#installation) for version selection):
- Maven:
- ```xml
-
- com.digitalsanctuary
- ds-spring-user-framework
- 3.3.0
-
+ **Spring Boot 4.0 (Java 21+):**
+ ```groovy
+ implementation 'com.digitalsanctuary:ds-spring-user-framework:4.0.0'
```
- Gradle:
+ **Spring Boot 3.5 (Java 17+):**
```groovy
- implementation 'com.digitalsanctuary:ds-spring-user-framework:3.3.0'
+ implementation 'com.digitalsanctuary:ds-spring-user-framework:3.5.1'
```
2. **Add required dependencies**:
@@ -663,6 +722,7 @@ We welcome contributions of all kinds! If you'd like to help improve SpringUserF
## Reference Documentation
- [API Documentation](https://digitalSanctuary.github.io/SpringUserFramework/)
+- [Migration Guide](MIGRATION.md)
- [Configuration Guide](CONFIG.md)
- [Security Guide](SECURITY.md)
- [Customization Guide](CUSTOMIZATION.md)
diff --git a/build.gradle b/build.gradle
index 35d75ca..6278d5f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,5 +1,5 @@
plugins {
- id 'org.springframework.boot' version '3.5.7'
+ id 'org.springframework.boot' version '4.0.0'
id 'io.spring.dependency-management' version '1.1.7'
id 'com.github.ben-manes.versions' version '0.53.0'
id 'java-library'
@@ -17,13 +17,12 @@ group = 'com.digitalsanctuary'
description = 'Spring User Framework'
ext {
- springBootVersion = '3.5.5'
lombokVersion = '1.18.42'
}
java {
toolchain {
- languageVersion = JavaLanguageVersion.of(17)
+ languageVersion = JavaLanguageVersion.of(21)
}
}
@@ -41,6 +40,8 @@ dependencies {
compileOnly 'org.springframework.boot:spring-boot-starter-security'
compileOnly 'org.springframework.boot:spring-boot-starter-thymeleaf'
compileOnly 'org.springframework.boot:spring-boot-starter-web'
+ // Note: thymeleaf-extras-springsecurity6 is compatible with Spring Security 7
+ // No springsecurity7 artifact exists yet - Thymeleaf team uses springsecurity6 for 6.x+
compileOnly 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.3.RELEASE'
compileOnly 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.4.0'
@@ -49,7 +50,7 @@ dependencies {
implementation 'com.google.guava:guava:33.5.0-jre'
implementation 'org.apache.commons:commons-text:1.15.0'
compileOnly 'jakarta.validation:jakarta.validation-api:3.1.1'
- compileOnly 'org.springframework.retry:spring-retry'
+ compileOnly 'org.springframework.retry:spring-retry:2.0.12'
// Lombok dependencies
compileOnly "org.projectlombok:lombok:$lombokVersion"
@@ -69,10 +70,16 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
testImplementation 'org.springframework.security:spring-security-test'
- testImplementation 'org.springframework.retry:spring-retry'
+ testImplementation 'org.springframework.retry:spring-retry:2.0.12'
testImplementation 'jakarta.validation:jakarta.validation-api:3.1.1'
+ testImplementation 'org.hibernate.validator:hibernate-validator:8.0.2.Final'
testImplementation 'com.h2database:h2:2.4.240'
+ // Spring Boot 4 test starters (modular test infrastructure)
+ testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test'
+ testImplementation 'org.springframework.boot:spring-boot-webmvc-test'
+ testImplementation 'org.springframework.boot:spring-boot-jdbc-test'
+
// Runtime dependencies moved to test scope for library
testRuntimeOnly 'org.springframework.boot:spring-boot-devtools'
testRuntimeOnly 'org.mariadb.jdbc:mariadb-java-client'
diff --git a/gradle.properties b/gradle.properties
index 9f38744..e1c17a6 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,3 +1,3 @@
-version=3.5.2-SNAPSHOT
+version=4.0.0-SNAPSHOT
mavenCentralPublishing=true
mavenCentralAutomaticPublishing=true
diff --git a/src/main/java/com/digitalsanctuary/spring/user/api/UserAPI.java b/src/main/java/com/digitalsanctuary/spring/user/api/UserAPI.java
index d6d0315..0aa3b9a 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/api/UserAPI.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/api/UserAPI.java
@@ -21,6 +21,7 @@
import com.digitalsanctuary.spring.user.dto.PasswordResetRequestDto;
import com.digitalsanctuary.spring.user.dto.SavePasswordDto;
import com.digitalsanctuary.spring.user.dto.UserDto;
+import com.digitalsanctuary.spring.user.dto.UserProfileUpdateDto;
import com.digitalsanctuary.spring.user.event.OnRegistrationCompleteEvent;
import com.digitalsanctuary.spring.user.exceptions.InvalidOldPasswordException;
import com.digitalsanctuary.spring.user.exceptions.UserAlreadyExistException;
@@ -131,24 +132,24 @@ public ResponseEntity resendRegistrationToken(@Valid @RequestBody
}
/**
- * Updates the user's password. This is used when the user is logged in and
- * wants to change their password.
+ * Updates the user's profile (first name, last name). This is used when the
+ * user is logged in and wants to update their profile information.
*
- * @param userDetails the authenticated user details
- * @param userDto the user data transfer object containing user details
- * @param request the HTTP servlet request
- * @param locale the locale
- * @return a ResponseEntity containing a JSONResponse with the password update
+ * @param userDetails the authenticated user details
+ * @param profileUpdateDto the profile update DTO containing first and last name
+ * @param request the HTTP servlet request
+ * @param locale the locale
+ * @return a ResponseEntity containing a JSONResponse with the profile update
* result
*/
@PostMapping("/updateUser")
public ResponseEntity updateUserAccount(@AuthenticationPrincipal DSUserDetails userDetails,
- @Valid @RequestBody UserDto userDto,
+ @Valid @RequestBody UserProfileUpdateDto profileUpdateDto,
HttpServletRequest request, Locale locale) {
validateAuthenticatedUser(userDetails);
User user = userDetails.getUser();
- user.setFirstName(userDto.getFirstName());
- user.setLastName(userDto.getLastName());
+ user.setFirstName(profileUpdateDto.getFirstName());
+ user.setLastName(profileUpdateDto.getLastName());
userService.saveRegisteredUser(user);
logAuditEvent("ProfileUpdate", "Success", "User profile updated", user, request);
diff --git a/src/main/java/com/digitalsanctuary/spring/user/dto/UserProfileUpdateDto.java b/src/main/java/com/digitalsanctuary/spring/user/dto/UserProfileUpdateDto.java
new file mode 100644
index 0000000..2cf4269
--- /dev/null
+++ b/src/main/java/com/digitalsanctuary/spring/user/dto/UserProfileUpdateDto.java
@@ -0,0 +1,23 @@
+package com.digitalsanctuary.spring.user.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+/**
+ * DTO for updating user profile information (first name, last name).
+ * This is separate from UserDto to avoid requiring password fields during profile updates.
+ */
+@Data
+public class UserProfileUpdateDto {
+
+ /** The first name. */
+ @NotBlank(message = "First name is required")
+ @Size(max = 50, message = "First name must not exceed 50 characters")
+ private String firstName;
+
+ /** The last name. */
+ @NotBlank(message = "Last name is required")
+ @Size(max = 50, message = "Last name must not exceed 50 characters")
+ private String lastName;
+}
diff --git a/src/main/java/com/digitalsanctuary/spring/user/security/WebSecurityConfig.java b/src/main/java/com/digitalsanctuary/spring/user/security/WebSecurityConfig.java
index 0f3488b..b201d1b 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/security/WebSecurityConfig.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/security/WebSecurityConfig.java
@@ -8,7 +8,6 @@
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
@@ -23,9 +22,7 @@
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
-import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.SecurityFilterChain;
-import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import com.digitalsanctuary.spring.user.roles.RolesAndPrivilegesConfig;
import com.digitalsanctuary.spring.user.service.DSOAuth2UserService;
@@ -279,18 +276,6 @@ public RoleHierarchy roleHierarchy() {
return roleHierarchy;
}
- /**
- * The webExpressionHandler method creates a DefaultWebSecurityExpressionHandler object and sets the roleHierarchy for the handler.
- *
- * @return the DefaultWebSecurityExpressionHandler object
- */
- @Bean
- public SecurityExpressionHandler webExpressionHandler() {
- DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler();
- defaultWebSecurityExpressionHandler.setRoleHierarchy(roleHierarchy());
- return defaultWebSecurityExpressionHandler;
- }
-
/**
* The methodSecurityExpressionHandler method creates a MethodSecurityExpressionHandler object and sets the roleHierarchy for the handler. This
* ensures that method security annotations like @PreAuthorize use the configured role hierarchy.
@@ -298,9 +283,9 @@ public SecurityExpressionHandler webExpressionHandler() {
* @return the MethodSecurityExpressionHandler object
*/
@Bean
- public MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
+ static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
- expressionHandler.setRoleHierarchy(roleHierarchy());
+ expressionHandler.setRoleHierarchy(roleHierarchy);
return expressionHandler;
}
diff --git a/src/test/java/com/digitalsanctuary/spring/user/api/UserAPIUnitTest.java b/src/test/java/com/digitalsanctuary/spring/user/api/UserAPIUnitTest.java
index 22f9ddd..c151b95 100644
--- a/src/test/java/com/digitalsanctuary/spring/user/api/UserAPIUnitTest.java
+++ b/src/test/java/com/digitalsanctuary/spring/user/api/UserAPIUnitTest.java
@@ -12,6 +12,7 @@
import com.digitalsanctuary.spring.user.audit.AuditEvent;
import com.digitalsanctuary.spring.user.dto.PasswordDto;
import com.digitalsanctuary.spring.user.dto.UserDto;
+import com.digitalsanctuary.spring.user.dto.UserProfileUpdateDto;
import com.digitalsanctuary.spring.user.event.OnRegistrationCompleteEvent;
import com.digitalsanctuary.spring.user.exceptions.InvalidOldPasswordException;
import com.digitalsanctuary.spring.user.exceptions.UserAlreadyExistException;
@@ -45,6 +46,7 @@
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.http.HttpStatus;
+import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import java.util.Collections;
import java.util.Locale;
@@ -112,6 +114,7 @@ void setUp() {
testUserDto.setFirstName("Test");
testUserDto.setLastName("User");
testUserDto.setPassword("password123");
+ testUserDto.setMatchingPassword("password123");
testUserDto.setRole(1);
testUserDetails = new DSUserDetails(testUser);
@@ -227,14 +230,12 @@ void registerUserAccount_missingEmail() throws Exception {
// Given
testUserDto.setEmail(null);
- // When & Then
+ // When & Then - validation should reject null email with 400 Bad Request
mockMvc.perform(post("/user/registration")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(testUserDto))
.with(csrf()))
- .andExpect(status().isInternalServerError())
- .andExpect(jsonPath("$.success").value(false))
- .andExpect(jsonPath("$.code").value(5));
+ .andExpect(status().isBadRequest());
}
@Test
@@ -243,14 +244,12 @@ void registerUserAccount_missingPassword() throws Exception {
// Given
testUserDto.setPassword(null);
- // When & Then
+ // When & Then - validation should reject null password with 400 Bad Request
mockMvc.perform(post("/user/registration")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(testUserDto))
.with(csrf()))
- .andExpect(status().isInternalServerError())
- .andExpect(jsonPath("$.success").value(false))
- .andExpect(jsonPath("$.code").value(5));
+ .andExpect(status().isBadRequest());
}
@Test
@@ -487,7 +486,7 @@ class UserProfileTests {
@DisplayName("POST /user/updateUser - success")
void updateUser_success() throws Exception {
// Given
- UserDto updateDto = new UserDto();
+ UserProfileUpdateDto updateDto = new UserProfileUpdateDto();
updateDto.setFirstName("UpdatedFirst");
updateDto.setLastName("UpdatedLast");
@@ -533,7 +532,7 @@ public Object resolveArgument(org.springframework.core.MethodParameter parameter
@DisplayName("POST /user/updateUser - not authenticated")
void updateUser_notAuthenticated() throws Exception {
// Given
- UserDto updateDto = new UserDto();
+ UserProfileUpdateDto updateDto = new UserProfileUpdateDto();
updateDto.setFirstName("UpdatedFirst");
updateDto.setLastName("UpdatedLast");
@@ -547,6 +546,215 @@ void updateUser_notAuthenticated() throws Exception {
.andExpect(jsonPath("$.code").value(401))
.andExpect(jsonPath("$.messages[0]").value("User not logged in."));
}
+
+ @Test
+ @DisplayName("POST /user/updateUser - validation fails with blank firstName")
+ void updateUser_blankFirstName_fails() throws Exception {
+ // Given
+ UserProfileUpdateDto updateDto = new UserProfileUpdateDto();
+ updateDto.setFirstName(""); // Blank - should fail validation
+ updateDto.setLastName("UpdatedLast");
+
+ // Create a validator for the standalone setup
+ LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
+ validator.afterPropertiesSet();
+
+ // Mock the principal resolver to return our test user
+ mockMvc = MockMvcBuilders.standaloneSetup(userAPI)
+ .setValidator(validator)
+ .setCustomArgumentResolvers(new HandlerMethodArgumentResolver() {
+ @Override
+ public boolean supportsParameter(org.springframework.core.MethodParameter parameter) {
+ return parameter.getParameterType().equals(DSUserDetails.class);
+ }
+
+ @Override
+ public Object resolveArgument(org.springframework.core.MethodParameter parameter,
+ org.springframework.web.method.support.ModelAndViewContainer mavContainer,
+ org.springframework.web.context.request.NativeWebRequest webRequest,
+ org.springframework.web.bind.support.WebDataBinderFactory binderFactory) {
+ return testUserDetails;
+ }
+ })
+ .setControllerAdvice(new TestExceptionHandler())
+ .build();
+
+ // When & Then - validation should fail
+ mockMvc.perform(post("/user/updateUser")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(updateDto))
+ .with(csrf()))
+ .andExpect(status().isBadRequest());
+
+ verify(userService, never()).saveRegisteredUser(any(User.class));
+ }
+
+ @Test
+ @DisplayName("POST /user/updateUser - validation fails with blank lastName")
+ void updateUser_blankLastName_fails() throws Exception {
+ // Given
+ UserProfileUpdateDto updateDto = new UserProfileUpdateDto();
+ updateDto.setFirstName("UpdatedFirst");
+ updateDto.setLastName(""); // Blank - should fail validation
+
+ // Create a validator for the standalone setup
+ LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
+ validator.afterPropertiesSet();
+
+ // Mock the principal resolver to return our test user
+ mockMvc = MockMvcBuilders.standaloneSetup(userAPI)
+ .setValidator(validator)
+ .setCustomArgumentResolvers(new HandlerMethodArgumentResolver() {
+ @Override
+ public boolean supportsParameter(org.springframework.core.MethodParameter parameter) {
+ return parameter.getParameterType().equals(DSUserDetails.class);
+ }
+
+ @Override
+ public Object resolveArgument(org.springframework.core.MethodParameter parameter,
+ org.springframework.web.method.support.ModelAndViewContainer mavContainer,
+ org.springframework.web.context.request.NativeWebRequest webRequest,
+ org.springframework.web.bind.support.WebDataBinderFactory binderFactory) {
+ return testUserDetails;
+ }
+ })
+ .setControllerAdvice(new TestExceptionHandler())
+ .build();
+
+ // When & Then - validation should fail
+ mockMvc.perform(post("/user/updateUser")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(updateDto))
+ .with(csrf()))
+ .andExpect(status().isBadRequest());
+
+ verify(userService, never()).saveRegisteredUser(any(User.class));
+ }
+
+ @Test
+ @DisplayName("POST /user/updateUser - validation fails with firstName exceeding 50 characters")
+ void updateUser_firstNameTooLong_fails() throws Exception {
+ // Given
+ UserProfileUpdateDto updateDto = new UserProfileUpdateDto();
+ updateDto.setFirstName("A".repeat(51)); // 51 chars - exceeds 50 char limit
+ updateDto.setLastName("UpdatedLast");
+
+ // Create a validator for the standalone setup
+ LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
+ validator.afterPropertiesSet();
+
+ // Mock the principal resolver to return our test user
+ mockMvc = MockMvcBuilders.standaloneSetup(userAPI)
+ .setValidator(validator)
+ .setCustomArgumentResolvers(new HandlerMethodArgumentResolver() {
+ @Override
+ public boolean supportsParameter(org.springframework.core.MethodParameter parameter) {
+ return parameter.getParameterType().equals(DSUserDetails.class);
+ }
+
+ @Override
+ public Object resolveArgument(org.springframework.core.MethodParameter parameter,
+ org.springframework.web.method.support.ModelAndViewContainer mavContainer,
+ org.springframework.web.context.request.NativeWebRequest webRequest,
+ org.springframework.web.bind.support.WebDataBinderFactory binderFactory) {
+ return testUserDetails;
+ }
+ })
+ .setControllerAdvice(new TestExceptionHandler())
+ .build();
+
+ // When & Then - validation should fail
+ mockMvc.perform(post("/user/updateUser")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(updateDto))
+ .with(csrf()))
+ .andExpect(status().isBadRequest());
+
+ verify(userService, never()).saveRegisteredUser(any(User.class));
+ }
+
+ @Test
+ @DisplayName("POST /user/updateUser - validation fails with null fields")
+ void updateUser_nullFields_fails() throws Exception {
+ // Given
+ UserProfileUpdateDto updateDto = new UserProfileUpdateDto();
+ // Both fields are null - should fail validation
+
+ // Create a validator for the standalone setup
+ LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
+ validator.afterPropertiesSet();
+
+ // Mock the principal resolver to return our test user
+ mockMvc = MockMvcBuilders.standaloneSetup(userAPI)
+ .setValidator(validator)
+ .setCustomArgumentResolvers(new HandlerMethodArgumentResolver() {
+ @Override
+ public boolean supportsParameter(org.springframework.core.MethodParameter parameter) {
+ return parameter.getParameterType().equals(DSUserDetails.class);
+ }
+
+ @Override
+ public Object resolveArgument(org.springframework.core.MethodParameter parameter,
+ org.springframework.web.method.support.ModelAndViewContainer mavContainer,
+ org.springframework.web.context.request.NativeWebRequest webRequest,
+ org.springframework.web.bind.support.WebDataBinderFactory binderFactory) {
+ return testUserDetails;
+ }
+ })
+ .setControllerAdvice(new TestExceptionHandler())
+ .build();
+
+ // When & Then - validation should fail
+ mockMvc.perform(post("/user/updateUser")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(updateDto))
+ .with(csrf()))
+ .andExpect(status().isBadRequest());
+
+ verify(userService, never()).saveRegisteredUser(any(User.class));
+ }
+
+ @Test
+ @DisplayName("POST /user/updateUser - accepts maximum valid length names")
+ void updateUser_maxValidLength_succeeds() throws Exception {
+ // Given
+ UserProfileUpdateDto updateDto = new UserProfileUpdateDto();
+ updateDto.setFirstName("A".repeat(50)); // Exactly 50 chars - should be valid
+ updateDto.setLastName("B".repeat(50)); // Exactly 50 chars - should be valid
+
+ // Mock the principal resolver to return our test user
+ mockMvc = MockMvcBuilders.standaloneSetup(userAPI)
+ .setCustomArgumentResolvers(new HandlerMethodArgumentResolver() {
+ @Override
+ public boolean supportsParameter(org.springframework.core.MethodParameter parameter) {
+ return parameter.getParameterType().equals(DSUserDetails.class);
+ }
+
+ @Override
+ public Object resolveArgument(org.springframework.core.MethodParameter parameter,
+ org.springframework.web.method.support.ModelAndViewContainer mavContainer,
+ org.springframework.web.context.request.NativeWebRequest webRequest,
+ org.springframework.web.bind.support.WebDataBinderFactory binderFactory) {
+ return testUserDetails;
+ }
+ })
+ .setControllerAdvice(new TestExceptionHandler())
+ .build();
+
+ when(messageSource.getMessage(eq("message.update-user.success"), any(), any(Locale.class)))
+ .thenReturn("Profile updated successfully");
+ when(userService.saveRegisteredUser(any(User.class))).thenReturn(testUser);
+
+ // When & Then
+ mockMvc.perform(post("/user/updateUser")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(updateDto))
+ .with(csrf()))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.success").value(true));
+
+ verify(userService).saveRegisteredUser(any(User.class));
+ }
}
@Nested
@@ -603,21 +811,21 @@ void deleteAccount_notAuthenticated() throws Exception {
class SecurityValidationTests {
@Test
- @DisplayName("POST /user/registration - CSRF protection")
+ @DisplayName("POST /user/registration - CSRF protection (standalone MockMvc limitation)")
void registration_csrfProtection() throws Exception {
- // Note: In standalone MockMvc setup, CSRF protection is not enabled
- // This test would pass with @WebMvcTest but not with standalone setup
- // For now, we skip this test for standalone unit testing
- // CSRF protection should be tested in integration tests instead
-
- // Given - simulating missing required fields to get an error
+ // Note: In standalone MockMvc setup, CSRF protection is not enabled by default.
+ // This test verifies basic request handling. Actual CSRF protection should be
+ // tested in integration tests using @WebMvcTest or full Spring context.
+
+ // Given - simulating missing required fields to trigger validation error
testUserDto.setEmail(null);
-
- // When & Then
+
+ // When & Then - without CSRF token, request still reaches validation
+ // which fails with 400 Bad Request for missing email
mockMvc.perform(post("/user/registration")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(testUserDto)))
- .andExpect(status().is5xxServerError());
+ .andExpect(status().isBadRequest());
}
@Test
diff --git a/src/test/java/com/digitalsanctuary/spring/user/test/annotations/DatabaseTest.java b/src/test/java/com/digitalsanctuary/spring/user/test/annotations/DatabaseTest.java
index ebb76c2..717e4b5 100644
--- a/src/test/java/com/digitalsanctuary/spring/user/test/annotations/DatabaseTest.java
+++ b/src/test/java/com/digitalsanctuary/spring/user/test/annotations/DatabaseTest.java
@@ -1,8 +1,8 @@
package com.digitalsanctuary.spring.user.test.annotations;
import com.digitalsanctuary.spring.user.test.config.DatabaseTestConfiguration;
-import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
-import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
+import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;
diff --git a/src/test/java/com/digitalsanctuary/spring/user/test/annotations/IntegrationTest.java b/src/test/java/com/digitalsanctuary/spring/user/test/annotations/IntegrationTest.java
index cd4dab5..030e918 100644
--- a/src/test/java/com/digitalsanctuary/spring/user/test/annotations/IntegrationTest.java
+++ b/src/test/java/com/digitalsanctuary/spring/user/test/annotations/IntegrationTest.java
@@ -2,8 +2,8 @@
import com.digitalsanctuary.spring.user.test.app.TestApplication;
import com.digitalsanctuary.spring.user.test.config.*;
-import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureDataJpa;
-import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.data.jpa.test.autoconfigure.AutoConfigureDataJpa;
+import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
diff --git a/src/test/java/com/digitalsanctuary/spring/user/test/annotations/OAuth2Test.java b/src/test/java/com/digitalsanctuary/spring/user/test/annotations/OAuth2Test.java
index 9ddf4ad..e3b506d 100644
--- a/src/test/java/com/digitalsanctuary/spring/user/test/annotations/OAuth2Test.java
+++ b/src/test/java/com/digitalsanctuary/spring/user/test/annotations/OAuth2Test.java
@@ -4,7 +4,7 @@
import com.digitalsanctuary.spring.user.test.config.BaseTestConfiguration;
import com.digitalsanctuary.spring.user.test.config.OAuth2TestConfiguration;
import com.digitalsanctuary.spring.user.test.config.SecurityTestConfiguration;
-import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
diff --git a/src/test/java/com/digitalsanctuary/spring/user/test/annotations/SecurityTest.java b/src/test/java/com/digitalsanctuary/spring/user/test/annotations/SecurityTest.java
index ef44f24..bcb3d32 100644
--- a/src/test/java/com/digitalsanctuary/spring/user/test/annotations/SecurityTest.java
+++ b/src/test/java/com/digitalsanctuary/spring/user/test/annotations/SecurityTest.java
@@ -3,7 +3,7 @@
import com.digitalsanctuary.spring.user.test.app.TestApplication;
import com.digitalsanctuary.spring.user.test.config.BaseTestConfiguration;
import com.digitalsanctuary.spring.user.test.config.SecurityTestConfiguration;
-import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener;
diff --git a/src/test/java/com/digitalsanctuary/spring/user/test/app/TestApplication.java b/src/test/java/com/digitalsanctuary/spring/user/test/app/TestApplication.java
index b886f09..d3c456c 100644
--- a/src/test/java/com/digitalsanctuary/spring/user/test/app/TestApplication.java
+++ b/src/test/java/com/digitalsanctuary/spring/user/test/app/TestApplication.java
@@ -3,7 +3,7 @@
import com.digitalsanctuary.spring.user.UserConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.boot.persistence.autoconfigure.EntityScan;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;