git:20260323.935227a to git:20260324.a02e282
56 added, 16 removed. Audit A to A.
---
name: unit-test-security-authorization
description: Provides patterns for unit testing Spring Security with `@PreAuthorize`, `@Secured`, `@RolesAllowed`. Validates role-based access control and authorization policies. Use when testing security configurations and access control logic.
allowed-tools: Read, Write, Bash, Glob, Grep
---
# Unit Testing Security and Authorization
## Overview
This skill provides patterns for unit testing Spring Security authorization logic using `@PreAuthorize`, `@Secured`, `@RolesAllowed`, and custom permission evaluators. It covers testing role-based access control (RBAC), expression-based authorization, custom permission evaluators, and verifying access denied scenarios without full Spring Security context.
## When to Use
Use this skill when:
- Testing `@PreAuthorize` and `@Secured` method-level security
- Testing role-based access control (RBAC)
- Testing custom permission evaluators
- Verifying access denied scenarios
- Testing authorization with authenticated principals
- Want fast authorization tests without full Spring Security context
## Instructions
Follow these steps to test Spring Security authorization:
### 1. Set Up Security Testing Dependencies
- Add spring-security-test to your test dependencies along with JUnit 5 and AssertJ. See [references/setup.md](references/setup.md) for detailed configuration.
+ Add spring-security-test to your test dependencies:
- ### 2. Enable Method Security in Configuration
- Use `@EnableGlobalMethodSecurity(prePostEnabled = true)` to activate `@PreAuthorize` annotations.
+ ```xml
+ <dependency>
+ <groupId>org.springframework.security</groupId>
+ <artifactId>spring-security-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+ ```
- ### 3. Create Test with @WithMockUser
- Apply `@WithMockUser` annotation to simulate authenticated users with specific roles and authorities.
+ ### 2. Enable Method Security in Test Configuration
+ ```java
+ @Configuration
+ @EnableMethodSecurity
+ class TestSecurityConfig { }
+ ```
- ### 4. Test Both Allow and Deny Scenarios
- For each security rule, test that authorized users can access the method and unauthorized users receive AccessDeniedException.
+ ### 3. Test with `@WithMockUser`
+ ```java
+ @Test
+ @WithMockUser(roles = "ADMIN")
+ void shouldAllowAdminAccess() {
+ assertThatCode(() -> service.deleteUser(1L))
+ .doesNotThrowAnyException();
+ }
- ### 5. Test Expression-Based Authorization
- Verify complex expressions like `authentication.principal.username == #owner` work correctly.
+ @Test
+ @WithMockUser(roles = "USER")
+ void shouldDenyUserAccess() {
+ assertThatThrownBy(() -> service.deleteUser(1L))
+ .isInstanceOf(AccessDeniedException.class);
+ }
+ ```
- ### 6. Test Custom Permission Evaluators
- Unit test custom PermissionEvaluator implementations by creating Authentication objects and calling hasPermission directly.
+ ### 4. Test Custom Permission Evaluators
+ ```java
+ @Test
+ void shouldGrantPermissionToOwner() {
+ Authentication auth = new UsernamePasswordAuthenticationToken(
+ "alice", null, List.of(new SimpleGrantedAuthority("ROLE_USER"))
+ );
+ Document doc = new Document(1L, "Test", new User("alice"));
- ### 7. Verify Method Interactions
- Mock external dependencies and verify that security checks don't interfere with business logic.
+ boolean result = evaluator.hasPermission(auth, doc, "WRITE");
+ assertThat(result).isTrue();
+ }
+ ```
+ ### 5. Validate Security is Active
+ If tests pass unexpectedly, add this assertion to verify security is enforced:
+ ```java
+ @Test
+ void shouldRejectUnauthorizedWhenSecurityEnabled() {
+ assertThatThrownBy(() -> service.deleteUser(1L))
+ .isInstanceOf(AccessDeniedException.class);
+ }
+ ```
+
## Quick Reference
| Annotation | Description | Example |
|------------|-------------|---------|
| `@PreAuthorize` | Pre-invocation authorization | `@PreAuthorize("hasRole('ADMIN')")` |
| `@PostAuthorize` | Post-invocation authorization | `@PostAuthorize("returnObject.owner == authentication.name")` |
| `@Secured` | Simple role-based security | `@Secured("ROLE_ADMIN")` |
| `@RolesAllowed` | JSR-250 standard | `@RolesAllowed({"ADMIN", "MANAGER"})` |
| `@WithMockUser` | Test annotation | `@WithMockUser(roles = "ADMIN")` |
## Examples
- ### Basic @PreAuthorize Test
+ ### Basic `@PreAuthorize` Test
```java
@Service
public class UserService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) {
// delete logic
}
}
// Test
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminToDeleteUser() {
assertThatCode(() -> service.deleteUser(1L))
.doesNotThrowAnyException();
}
@Test
@WithMockUser(roles = "USER")
void shouldDenyUserFromDeletingUser() {
assertThatThrownBy(() -> service.deleteUser(1L))
.isInstanceOf(AccessDeniedException.class);
}
```
### Expression-Based Security Test
```java
@PreAuthorize("#userId == authentication.principal.id")
public UserProfile getUserProfile(Long userId) {
// get profile
}
- // Test
+ // For custom principal properties, use @WithUserDetails with a custom UserDetailsService
@Test
- @WithMockUser(username = "alice", id = "1")
+ @WithUserDetails("alice")
void shouldAllowUserToAccessOwnProfile() {
assertThatCode(() -> service.getUserProfile(1L))
.doesNotThrowAnyException();
}
```
+
+ > **Validation tip**: If a security test passes unexpectedly, verify that `@EnableMethodSecurity` is active on the test configuration — a missing annotation causes all `@PreAuthorize` checks to be bypassed silently.
See [references/basic-testing.md](references/basic-testing.md) for more basic patterns and [references/advanced-authorization.md](references/advanced-authorization.md) for complex expressions and custom evaluators.
## Best Practices
1. **Use `@WithMockUser`** for setting authenticated user context
2. **Test both allow and deny cases** for each security rule
3. **Test with different roles** to verify role-based decisions
4. **Test expression-based security** comprehensively
5. **Mock external dependencies** (permission evaluators, etc.)
6. **Test anonymous access separately** from authenticated access
7. **Use `@EnableGlobalMethodSecurity`** in configuration for method-level security
## Common Pitfalls
- Forgetting to enable method security in test configuration
- Not testing both allow and deny scenarios
- Testing framework code instead of authorization logic
- Not handling null authentication in tests
- Mixing authentication and authorization tests unnecessarily
## Constraints and Warnings
- **Method security requires proxy**: `@PreAuthorize` works via proxies; direct method calls bypass security
- **`@EnableGlobalMethodSecurity`**: Must be enabled for `@PreAuthorize`, `@Secured` to work
- **Role prefix**: Spring adds "ROLE_" prefix automatically; use `hasRole('ADMIN')` not `hasRole('ROLE_ADMIN')`
- **Authentication context**: Security context is thread-local; be careful with async tests
- **`@WithMockUser` limitations**: Creates a simple Authentication; complex auth scenarios need custom setup
- **SpEL expressions**: Complex SpEL in `@PreAuthorize` can be difficult to debug; test thoroughly
- **Performance impact**: Method security adds overhead; consider security at layer boundaries
## References
### Setup and Configuration
- **[references/setup.md](references/setup.md)** - Maven/Gradle dependencies and security configuration
### Testing Patterns
- **[references/basic-testing.md](references/basic-testing.md)** - Basic patterns for `@PreAuthorize`, `@Secured`, MockMvc testing, and parameterized tests
### Advanced Topics
- **[references/advanced-authorization.md](references/advanced-authorization.md)** - Expression-based authorization, custom permission evaluators, SpEL expressions
### Complete Examples
- **[references/complete-examples.md](references/complete-examples.md)** - Before/after examples showing transition from manual to declarative security