Week 6 Day 5 — Security Testing
Goal
Today I want to understand how to test Spring Security.
Main questions:
- Why do security tests matter?
- What dependency do I need?
- How do I test secured controllers with MockMvc?
- What is
@WithMockUser? - How do I test roles and authorities?
- How do I test 401 and 403?
- How do I test CSRF?
- How do I test HTTP Basic?
- How do I test method security?
- How do I test
@PreAuthorize? - How do I test custom principal logic?
- What are common security test bugs?
1. Quick Review from Week 6 Day 4
In Day 4, I learned:
- CSRF means Cross-Site Request Forgery.
- CSRF matters especially with browser cookies and sessions.
- Stateless bearer-token APIs commonly disable CSRF, but not blindly.
- CORS controls which browser origins may call the API.
- Session security stores authentication state on the server.
- Stateless security authenticates every request independently.
SessionCreationPolicy.STATELESSmeans no HTTP session for auth state.- JWT has header, payload, and signature.
- JWT is signed, not necessarily encrypted.
- Bearer token means possession grants access.
- Public endpoints should be explicit.
- Everything else should usually be authenticated.
Memory sentence:
Public by exception, protected by default.
Today I learn how to prove security rules with tests.
2. Why Security Tests Matter
Security bugs are dangerous.
Examples:
public endpoint accidentally protected
protected endpoint accidentally public
USER can access ADMIN endpoint
missing CSRF token still accepted
POST request fails in tests with 403
method security not enabled
@PreAuthorize not tested
JWT authorities mapped incorrectly
custom principal missing in test
Security tests help check:
401 when not authenticated
403 when authenticated but not allowed
200 when allowed
CSRF behavior
roles and authorities
method security
custom authorization logic
Memory sentence:
Security tests prove that allowed users are allowed and forbidden users are blocked.
3. Security Testing Dependency
Add this test dependency:
testImplementation("org.springframework.security:spring-security-test")
In Maven:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
This gives useful test tools:
@WithMockUser
@WithAnonymousUser
@WithUserDetails
SecurityMockMvcRequestPostProcessors
csrf()
httpBasic()
user()
jwt()
oauth2Login()
Memory sentence:
spring-security-testgives test helpers for Spring Security.
4. Types of Security Tests
Common security test types:
controller security tests
URL authorization tests
CSRF tests
method security tests
JWT/resource server tests
custom permission tests
integration tests
Example questions:
Can anonymous user access public endpoint?
Can anonymous user access protected endpoint?
Can USER access user endpoint?
Can USER access admin endpoint?
Can ADMIN access admin endpoint?
Does POST fail without CSRF when CSRF is enabled?
Does @PreAuthorize block unauthorized service call?
Memory sentence:
Test both successful access and denied access.
5. MockMvc Security Testing
MockMvc lets me test MVC controllers without starting a real server.
Example:
@SpringBootTest
@AutoConfigureMockMvc
class TaskControllerSecurityTest {
@Autowired
private MockMvc mockMvc;
}
With Spring Boot and @AutoConfigureMockMvc, Spring Security filters are usually included.
Then I can test HTTP requests:
mockMvc.perform(get("/api/tasks"))
.andExpect(status().isUnauthorized());
Memory sentence:
MockMvc can test secured endpoints through the Spring Security filter chain.
6. MockMvc with Manual Setup
Sometimes I configure MockMvc manually.
Then I need:
.apply(springSecurity())
Example:
@BeforeEach
void setup(WebApplicationContext context) {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
This integrates Spring Security with MockMvc.
Memory sentence:
If MockMvc is built manually, apply
springSecurity().
7. Test Example Controller
Controller:
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
@GetMapping
public List<String> list() {
return List.of("Task A", "Task B");
}
@PostMapping
public ResponseEntity<String> create(@RequestBody String body) {
return ResponseEntity.status(HttpStatus.CREATED).body("created");
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
return ResponseEntity.noContent().build();
}
}
Security config:
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/api/tasks/**").hasAuthority("TASK_READ")
.requestMatchers(HttpMethod.POST, "/api/tasks/**").hasAuthority("TASK_WRITE")
.requestMatchers(HttpMethod.DELETE, "/api/tasks/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.build();
}
Rules:
GET /api/tasks needs TASK_READ
POST /api/tasks needs TASK_WRITE
DELETE /api/tasks/{id} needs ADMIN role
everything else needs authentication
8. @WithMockUser
@WithMockUser runs a test as a mocked authenticated user.
Example:
@Test
@WithMockUser
void authenticatedUserCanCallEndpoint() throws Exception {
mockMvc.perform(get("/api/profile"))
.andExpect(status().isOk());
}
Default mock user:
username = user
password = password
role = USER
authority = ROLE_USER
Memory sentence:
@WithMockUsercreates an authenticated user for the test.
9. @WithMockUser with Roles
Example:
@Test
@WithMockUser(username = "admin@example.com", roles = "ADMIN")
void adminCanDeleteTask() throws Exception {
mockMvc.perform(delete("/api/tasks/1")
.with(csrf()))
.andExpect(status().isNoContent());
}
Important:
roles = "ADMIN"
creates authority:
ROLE_ADMIN
Memory sentence:
@WithMockUser(roles = "ADMIN")gives authorityROLE_ADMIN.
10. @WithMockUser with Authorities
Example:
@Test
@WithMockUser(authorities = "TASK_READ")
void userWithTaskReadCanListTasks() throws Exception {
mockMvc.perform(get("/api/tasks"))
.andExpect(status().isOk());
}
This gives exact authority:
TASK_READ
Memory sentence:
Use
authoritieswhen testing fine-grained permissions.
11. Roles vs Authorities in Tests
Role test:
@WithMockUser(roles = "ADMIN")
creates:
ROLE_ADMIN
Authority test:
@WithMockUser(authorities = "ROLE_ADMIN")
creates exactly:
ROLE_ADMIN
Authority test:
@WithMockUser(authorities = "TASK_READ")
creates exactly:
TASK_READ
Common mistake:
@WithMockUser(roles = "ROLE_ADMIN")
Wrong, because roles add ROLE_.
Memory sentence:
In tests, roles add
ROLE_; authorities are exact.
12. Testing 401 Unauthorized
401 means:
not authenticated
Test:
@Test
void anonymousCannotAccessProtectedEndpoint() throws Exception {
mockMvc.perform(get("/api/tasks"))
.andExpect(status().isUnauthorized());
}
No @WithMockUser.
No authentication.
Expected result:
401 Unauthorized
Memory sentence:
To test 401, make the request without authentication.
13. Testing 403 Forbidden
403 means:
authenticated but not allowed
Example:
@Test
@WithMockUser(roles = "USER")
void normalUserCannotDeleteTask() throws Exception {
mockMvc.perform(delete("/api/tasks/1")
.with(csrf()))
.andExpect(status().isForbidden());
}
Why?
user is authenticated
but DELETE requires ADMIN
Memory sentence:
To test 403, authenticate as a user without enough permission.
14. Testing 200 OK
Example:
@Test
@WithMockUser(authorities = "TASK_READ")
void userWithTaskReadCanListTasks() throws Exception {
mockMvc.perform(get("/api/tasks"))
.andExpect(status().isOk());
}
Why?
GET /api/tasks requires TASK_READ
mock user has TASK_READ
Memory sentence:
A good security test includes a success case.
15. Testing 201 Created with CSRF
If CSRF is enabled, unsafe HTTP methods need CSRF token.
Unsafe methods:
POST
PUT
PATCH
DELETE
Test:
@Test
@WithMockUser(authorities = "TASK_WRITE")
void userWithTaskWriteCanCreateTask() throws Exception {
mockMvc.perform(post("/api/tasks")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Learn Security Testing"}
"""))
.andExpect(status().isCreated());
}
Memory sentence:
If CSRF is enabled, POST tests need
.with(csrf()).
16. Testing Missing CSRF
Example:
@Test
@WithMockUser(authorities = "TASK_WRITE")
void postWithoutCsrfIsForbiddenWhenCsrfEnabled() throws Exception {
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Learn Security Testing"}
"""))
.andExpect(status().isForbidden());
}
Expected result:
403 Forbidden
Why?
user is authenticated
user has authority
but CSRF token is missing
Memory sentence:
Missing CSRF token can cause 403 even when the user has permission.
17. Testing Invalid CSRF
Example:
@Test
@WithMockUser(authorities = "TASK_WRITE")
void postWithInvalidCsrfIsForbidden() throws Exception {
mockMvc.perform(post("/api/tasks")
.with(csrf().useInvalidToken())
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Invalid CSRF"}
"""))
.andExpect(status().isForbidden());
}
Memory sentence:
Tests can check invalid CSRF tokens too.
18. When CSRF Is Disabled
If security config has:
.csrf(csrf -> csrf.disable())
then POST tests do not need:
.with(csrf())
Example:
@Test
@WithMockUser(authorities = "TASK_WRITE")
void postWorksWithoutCsrfWhenCsrfDisabled() throws Exception {
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Stateless API"}
"""))
.andExpect(status().isCreated());
}
Memory sentence:
Whether tests need CSRF depends on security configuration.
19. Testing HTTP Basic
If config enables HTTP Basic:
.httpBasic(Customizer.withDefaults())
I can test with:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
Example:
@Test
void validBasicAuthCanAccessEndpoint() throws Exception {
mockMvc.perform(get("/api/tasks")
.with(httpBasic("user@example.com", "password")))
.andExpect(status().isOk());
}
This tests real username/password authentication if users are configured.
Memory sentence:
httpBasic()tests Basic authentication through Spring Security.
20. @WithMockUser vs httpBasic
@WithMockUser:
puts a mock authenticated user into security context
does not test password checking
fast and simple
good for authorization tests
httpBasic():
sends username/password
tests authentication flow
uses UserDetailsService and PasswordEncoder
better for login/authentication integration tests
Memory sentence:
Use
@WithMockUserfor authorization,httpBasic()for authentication flow.
21. Testing Public Endpoints
Public endpoint:
.requestMatchers("/api/auth/**").permitAll()
Test:
@Test
void anonymousCanAccessLoginEndpoint() throws Exception {
mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"email":"user@example.com","password":"password"}
"""))
.andExpect(status().isOk());
}
If CSRF is enabled, this POST may still need:
.with(csrf())
unless CSRF is disabled or ignored for that endpoint.
Memory sentence:
permitAlldoes not automatically disable CSRF.
22. Important Trap: permitAll vs CSRF
Config:
.requestMatchers("/api/auth/**").permitAll()
This means:
no authentication required
But if CSRF is enabled:
POST /api/auth/login may still need CSRF token
Memory sentence:
permitAllbypasses authentication, not necessarily CSRF.
23. Testing Anonymous User Explicitly
I can use:
@WithAnonymousUser
Example:
@Test
@WithAnonymousUser
void anonymousCannotAccessTasks() throws Exception {
mockMvc.perform(get("/api/tasks"))
.andExpect(status().isUnauthorized());
}
This makes the anonymous case explicit.
Memory sentence:
@WithAnonymousUsermakes anonymous tests clear.
24. Testing Controller with Current User
Controller:
@GetMapping("/api/me")
public MeDto me(Authentication authentication) {
return new MeDto(authentication.getName());
}
Test:
@Test
@WithMockUser(username = "steve@example.com")
void returnsCurrentUser() throws Exception {
mockMvc.perform(get("/api/me"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").value("steve@example.com"));
}
DTO:
public record MeDto(String email) {
}
Memory sentence:
@WithMockUser(username = "...")controlsauthentication.getName().
25. Testing Authorities in Controller
Controller:
@GetMapping("/api/me/authorities")
public List<String> authorities(Authentication authentication) {
return authentication.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.toList();
}
Test:
@Test
@WithMockUser(authorities = {"TASK_READ", "TASK_WRITE"})
void returnsAuthorities() throws Exception {
mockMvc.perform(get("/api/me/authorities"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0]").value("TASK_READ"))
.andExpect(jsonPath("$[1]").value("TASK_WRITE"));
}
Memory sentence:
@WithMockUser(authorities = ...)controls exact authorities.
26. Testing Method Security
Method security protects service methods.
Example service:
@Service
public class AdminService {
@PreAuthorize("hasRole('ADMIN')")
public String adminOnly() {
return "secret";
}
}
Enable method security:
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
}
Test:
@SpringBootTest
class AdminServiceSecurityTest {
@Autowired
private AdminService adminService;
@Test
@WithMockUser(roles = "ADMIN")
void adminCanCallMethod() {
assertThat(adminService.adminOnly()).isEqualTo("secret");
}
@Test
@WithMockUser(roles = "USER")
void userCannotCallMethod() {
assertThatThrownBy(() -> adminService.adminOnly())
.isInstanceOf(AccessDeniedException.class);
}
}
Memory sentence:
Method security tests call the service method and expect success or
AccessDeniedException.
27. Testing @PreAuthorize with Authorities
Service:
@Service
public class TaskService {
@PreAuthorize("hasAuthority('TASK_DELETE')")
public void deleteTask(Long taskId) {
// delete task
}
}
Test allowed:
@Test
@WithMockUser(authorities = "TASK_DELETE")
void userWithAuthorityCanDeleteTask() {
taskService.deleteTask(1L);
}
Test denied:
@Test
@WithMockUser(authorities = "TASK_READ")
void userWithoutAuthorityCannotDeleteTask() {
assertThatThrownBy(() -> taskService.deleteTask(1L))
.isInstanceOf(AccessDeniedException.class);
}
Memory sentence:
Test both matching and non-matching authorities.
28. Testing @PreAuthorize with Method Parameters
Service:
@Service
public class UserProfileService {
@PreAuthorize("#userId == authentication.principal.id")
public String getProfile(Long userId) {
return "profile";
}
}
This needs custom principal with id.
@WithMockUser creates a normal Spring Security user, not my custom principal.
So this test may fail:
@WithMockUser
because:
authentication.principal.id does not exist
Memory sentence:
@WithMockUseris not enough when expressions need custom principal fields.
29. Testing Custom Principal with Request PostProcessor
For controller tests, I can use:
.with(user(customPrincipal))
Custom principal:
AppUserPrincipal principal = new AppUserPrincipal(
10L,
5L,
"steve@example.com",
List.of(new SimpleGrantedAuthority("ROLE_USER")),
true
);
Test:
@Test
void currentUserCanAccessOwnTenant() throws Exception {
AppUserPrincipal principal = new AppUserPrincipal(
10L,
5L,
"steve@example.com",
List.of(new SimpleGrantedAuthority("ROLE_USER")),
true
);
mockMvc.perform(get("/api/tenants/5/tasks")
.with(user(principal)))
.andExpect(status().isOk());
}
Memory sentence:
Use
.with(user(customPrincipal))when controller needs custom principal.
30. Testing Custom Principal with @WithSecurityContext
For method security tests with custom principal, I can create a custom annotation using:
@WithSecurityContext
Example annotation:
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockAppUserSecurityContextFactory.class)
public @interface WithMockAppUser {
long id() default 1L;
long tenantId() default 1L;
String email() default "user@example.com";
String[] authorities() default {"ROLE_USER"};
}
Factory:
public class WithMockAppUserSecurityContextFactory
implements WithSecurityContextFactory<WithMockAppUser> {
@Override
public SecurityContext createSecurityContext(WithMockAppUser annotation) {
List<GrantedAuthority> authorities = Arrays.stream(annotation.authorities())
.map(SimpleGrantedAuthority::new)
.toList();
AppUserPrincipal principal = new AppUserPrincipal(
annotation.id(),
annotation.tenantId(),
annotation.email(),
authorities,
true
);
Authentication authentication =
new UsernamePasswordAuthenticationToken(
principal,
"password",
authorities
);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
return context;
}
}
Usage:
@Test
@WithMockAppUser(id = 10L, tenantId = 5L, authorities = {"TASK_READ"})
void canAccessOwnTenantData() {
taskService.findTenantTasks(5L);
}
Memory sentence:
Use custom
@WithSecurityContextfor custom principal method security tests.
31. Testing Custom Permission Service
Permission service:
@Component("permissionService")
public class PermissionService {
public boolean canAccessTask(Long taskId, Authentication authentication) {
AppUserPrincipal principal =
(AppUserPrincipal) authentication.getPrincipal();
return principal.getTenantId().equals(5L);
}
}
Service:
@PreAuthorize("@permissionService.canAccessTask(#taskId, authentication)")
public TaskDto findTask(Long taskId) {
return new TaskDto(taskId, "Test task");
}
Test:
@Test
@WithMockAppUser(tenantId = 5L)
void userWithPermissionCanAccessTask() {
TaskDto result = taskService.findTask(1L);
assertThat(result.id()).isEqualTo(1L);
}
Denied test:
@Test
@WithMockAppUser(tenantId = 99L)
void userWithoutPermissionCannotAccessTask() {
assertThatThrownBy(() -> taskService.findTask(1L))
.isInstanceOf(AccessDeniedException.class);
}
Memory sentence:
Custom permission logic should have allowed and denied tests.
32. Testing JWT Resource Server with MockMvc
For JWT-based APIs, Spring Security test support can mock a JWT.
Example:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
Test:
@Test
void userWithJwtCanReadTasks() throws Exception {
mockMvc.perform(get("/api/tasks")
.with(jwt().authorities(
new SimpleGrantedAuthority("SCOPE_task:read")
)))
.andExpect(status().isOk());
}
This does not require a real token.
It creates a mocked JWT authentication for the test.
Memory sentence:
Use
jwt()to test resource-server authorization without creating a real JWT.
33. Testing JWT Claims
Example:
@Test
void jwtWithTenantClaimCanAccessTenantEndpoint() throws Exception {
mockMvc.perform(get("/api/tenants/5/tasks")
.with(jwt()
.jwt(jwt -> jwt
.subject("steve@example.com")
.claim("tenantId", 5L)
)
.authorities(new SimpleGrantedAuthority("SCOPE_task:read"))
))
.andExpect(status().isOk());
}
Useful when controller or converter uses JWT claims.
Memory sentence:
JWT tests can mock claims and authorities.
34. Testing Denied JWT Authority
Endpoint requires:
.hasAuthority("SCOPE_task:write")
Test:
@Test
void jwtWithoutWriteScopeCannotCreateTask() throws Exception {
mockMvc.perform(post("/api/tasks")
.with(jwt().authorities(
new SimpleGrantedAuthority("SCOPE_task:read")
))
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"New task"}
"""))
.andExpect(status().isForbidden());
}
If CSRF is enabled, add:
.with(csrf())
If stateless JWT config disables CSRF, no CSRF is needed.
Memory sentence:
JWT authority tests should check allowed and forbidden scopes.
35. Testing Login Endpoint
Login controller uses:
AuthenticationManager
Test with real configured user:
@Test
void loginWithValidCredentialsReturnsToken() throws Exception {
mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"email": "user@example.com",
"password": "password"
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.token").exists());
}
Bad password:
@Test
void loginWithBadPasswordReturnsUnauthorized() throws Exception {
mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"email": "user@example.com",
"password": "wrong"
}
"""))
.andExpect(status().isUnauthorized());
}
If CSRF is enabled, login POST may need .with(csrf()).
Memory sentence:
Login tests should check valid and invalid credentials.
36. Testing with @WebMvcTest
@WebMvcTest loads only MVC-related components.
Example:
@WebMvcTest(TaskController.class)
class TaskControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private TaskService taskService;
}
With security enabled, @WebMvcTest can still apply security filters.
If the controller depends on security config or custom beans, I may need:
@Import(SecurityConfig.class)
or mock missing beans.
Memory sentence:
@WebMvcTestis sliced; import or mock security-related dependencies as needed.
37. Testing with @SpringBootTest
@SpringBootTest loads the full application context.
Example:
@SpringBootTest
@AutoConfigureMockMvc
class SecurityIntegrationTest {
@Autowired
private MockMvc mockMvc;
}
Good for:
full security configuration
real filters
method security
repository/service integration
login flow
JWT/resource server config
Slower than @WebMvcTest.
Memory sentence:
Use
@SpringBootTestfor full security integration tests.
38. Choosing Test Type
| Test Type | Good For |
|---|---|
@WebMvcTest | controller + URL security slice |
@SpringBootTest | full security integration |
service test with @WithMockUser | method security |
MockMvc with jwt() | resource server authorization |
MockMvc with httpBasic() | Basic authentication flow |
custom @WithSecurityContext | custom principal tests |
Memory sentence:
Choose the smallest test that proves the security rule.
39. Testing @AuthenticationPrincipal
Controller:
@GetMapping("/api/me")
public MeDto me(@AuthenticationPrincipal AppUserPrincipal principal) {
return new MeDto(principal.getId(), principal.getUsername());
}
Test:
@Test
void returnsCustomPrincipalData() throws Exception {
AppUserPrincipal principal = new AppUserPrincipal(
10L,
5L,
"steve@example.com",
List.of(new SimpleGrantedAuthority("ROLE_USER")),
true
);
mockMvc.perform(get("/api/me")
.with(user(principal)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(10))
.andExpect(jsonPath("$.email").value("steve@example.com"));
}
Memory sentence:
For
@AuthenticationPrincipal, test with the same principal type the controller expects.
40. Testing Current User Passed to Service
Controller:
@GetMapping("/api/tasks")
public List<TaskDto> list(@AuthenticationPrincipal AppUserPrincipal principal) {
return taskService.listForTenant(principal.getTenantId());
}
Test:
@Test
void passesTenantIdToService() throws Exception {
AppUserPrincipal principal = new AppUserPrincipal(
10L,
5L,
"steve@example.com",
List.of(new SimpleGrantedAuthority("ROLE_USER")),
true
);
when(taskService.listForTenant(5L))
.thenReturn(List.of(new TaskDto(1L, "Task A")));
mockMvc.perform(get("/api/tasks")
.with(user(principal)))
.andExpect(status().isOk());
verify(taskService).listForTenant(5L);
}
Memory sentence:
Controller security tests can verify that current-user data is passed correctly.
41. Common Bug: Test Passes Without Security
Bug:
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
This may not include Spring Security filters.
A test may pass even though real app would block the request.
Fix:
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
or use:
@SpringBootTest
@AutoConfigureMockMvc
Memory sentence:
Make sure security filters are actually active in security tests.
42. Common Bug: Expecting 401 but Getting 403
Possible causes:
CSRF token missing
user is authenticated but lacks permission
anonymous handling differs by config
request has invalid auth
Example:
@Test
@WithMockUser
void postFails() throws Exception {
mockMvc.perform(post("/api/tasks"))
.andExpect(status().isUnauthorized());
}
But actual result:
403
Why?
@WithMockUser means authenticated
POST may be missing CSRF
or user lacks TASK_WRITE
Memory sentence:
401 means no authentication; 403 means authenticated but blocked or CSRF denied.
43. Common Bug: Forgetting CSRF in POST Tests
Bug:
@Test
@WithMockUser(authorities = "TASK_WRITE")
void createTask() throws Exception {
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Task A"}
"""))
.andExpect(status().isCreated());
}
Actual result:
403 Forbidden
Fix:
.with(csrf())
if CSRF is enabled.
Memory sentence:
403 on POST with correct authority often means missing CSRF.
44. Common Bug: Using Roles Wrong
Bug:
@WithMockUser(roles = "ROLE_ADMIN")
Expected:
ROLE_ADMIN
But roles add prefix and may become:
ROLE_ROLE_ADMIN
Fix:
@WithMockUser(roles = "ADMIN")
or:
@WithMockUser(authorities = "ROLE_ADMIN")
Memory sentence:
In
@WithMockUser, roles should not includeROLE_.
45. Common Bug: Method Security Not Enabled
Service:
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) {
}
Test expects AccessDeniedException, but method runs.
Possible reason:
@EnableMethodSecurity is missing
Fix:
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
}
Memory sentence:
@PreAuthorizeneeds method security enabled.
46. Common Bug: Testing Non-Spring Instance
Bad:
TaskService service = new TaskService(repository);
service.deleteTask(1L);
This bypasses Spring proxy.
Method security may not apply.
Correct:
@Autowired
private TaskService taskService;
Use the Spring bean from context.
Memory sentence:
Method security works on Spring-managed beans, not manually created objects.
47. Common Bug: Self-Invocation
Service:
public void outer() {
inner();
}
@PreAuthorize("hasRole('ADMIN')")
public void inner() {
}
If outer() calls inner() inside same class, method security may be bypassed.
Test should not only test inner() directly.
Also test real public entry points.
Memory sentence:
Test the method path that production code really uses.
48. Common Bug: @WithMockUser Does Not Use Database
@WithMockUser creates a mock user.
It does not load user from database.
So this test:
@WithMockUser(username = "real@example.com")
does not prove:
real@example.com exists in database
password is correct
UserDetailsService works
For that, use:
httpBasic()
formLogin()
custom login endpoint
@WithUserDetails
full integration test
Memory sentence:
@WithMockUsertests authorization context, not real login.
49. @WithUserDetails
@WithUserDetails loads a real user through UserDetailsService.
Example:
@Test
@WithUserDetails("user@example.com")
void realUserDetailsCanAccessEndpoint() throws Exception {
mockMvc.perform(get("/api/tasks"))
.andExpect(status().isOk());
}
Requirement:
user must exist in test database or test UserDetailsService
Memory sentence:
@WithUserDetailsuses the realUserDetailsService.
50. @WithMockUser vs @WithUserDetails
| Annotation | User Source | Good For |
|---|---|---|
@WithMockUser | fake test user | authorization tests |
@WithUserDetails | real UserDetailsService | tests needing real user loading |
custom @WithSecurityContext | custom factory | custom principal tests |
Memory sentence:
Pick the test user strategy based on what you need to prove.
51. Security Test Checklist
Use this checklist:
[ ] Public endpoints are accessible anonymously.
[ ] Protected endpoints return 401 anonymously.
[ ] Authenticated users can access allowed endpoints.
[ ] Authenticated users without permission get 403.
[ ] Admin endpoints require admin role.
[ ] Role prefix is tested correctly.
[ ] Authority rules are tested correctly.
[ ] POST/PUT/PATCH/DELETE tests include CSRF if CSRF is enabled.
[ ] Missing CSRF is tested where important.
[ ] Method security allowed case is tested.
[ ] Method security denied case is tested.
[ ] Custom permission service is tested.
[ ] Custom principal tests use correct principal type.
[ ] JWT tests include required authorities/scopes.
[ ] Security filters are active in MockMvc tests.
[ ] Tests cover both success and failure paths.
52. Real Exam Question: Security Test Dependency
Question:
Which dependency provides Spring Security test helpers?
Answer:
spring-security-test.
53. Real Exam Question: @WithMockUser
Question:
What does @WithMockUser do?
Answer:
It runs the test with a mocked authenticated user in the security context.
54. Real Exam Question: Default @WithMockUser
Question:
What role does default @WithMockUser have?
Answer:
The default mock user has role USER, which means authority ROLE_USER.
55. Real Exam Question: CSRF in Tests
Question:
How do I add a CSRF token in MockMvc?
Answer:
Use .with(csrf()).
56. Real Exam Question: Missing CSRF
Question:
What status can a POST request return if CSRF is enabled and token is missing?
Answer:
403 Forbidden.
57. Real Exam Question: 401 vs 403 in Tests
Question:
How do I test 401?
Answer:
Send the request without authentication.
Question:
How do I test 403?
Answer:
Authenticate as a user who lacks the required permission, or send an invalid/missing CSRF token for unsafe methods when CSRF is enabled.
58. Real Exam Question: Method Security Test
Question:
How do I test a method protected with @PreAuthorize?
Answer:
Call the Spring-managed service bean with a mock authenticated user and assert either success or AccessDeniedException.
59. Real Exam Question: @WithUserDetails
Question:
What is the difference between @WithMockUser and @WithUserDetails?
Answer:
@WithMockUser creates a fake user. @WithUserDetails loads a real user using UserDetailsService.
60. Interview Answer
Question:
How do you test endpoint authorization with MockMvc?
Good answer:
I use MockMvc with Spring Security test support. For anonymous access, I make the request without authentication and expect 401 for protected endpoints. For allowed users, I use @WithMockUser or request post processors like user() or jwt() with the required roles or authorities and expect 200. For forbidden users, I authenticate with insufficient authorities and expect 403. For unsafe HTTP methods, I include .with(csrf()) if CSRF is enabled.
61. Interview Answer
Question:
What is the difference between
@WithMockUser,httpBasic(), andjwt()in tests?
Good answer:
@WithMockUser directly creates a mock authenticated user in the security context, so it is good for authorization tests. httpBasic() sends username and password through the actual Basic authentication flow, so it can test UserDetailsService and PasswordEncoder. jwt() creates a mock JWT authentication, useful for testing OAuth2 Resource Server or bearer-token authorization without generating a real token.
62. Interview Answer
Question:
Why do POST tests often fail with 403?
Good answer:
If CSRF protection is enabled, unsafe HTTP methods such as POST, PUT, PATCH, and DELETE require a valid CSRF token. Even if the user is authenticated and has the right role, the request can fail with 403 if the CSRF token is missing. In MockMvc tests, I add .with(csrf()).
63. Interview Answer
Question:
How do you test method security?
Good answer:
I enable method security with @EnableMethodSecurity, load the Spring-managed service bean, and use annotations like @WithMockUser or a custom @WithSecurityContext to set the security context. Then I test both allowed and denied cases. Allowed users should get the result; denied users should get AccessDeniedException.
64. Interview Answer
Question:
What are common security testing mistakes?
Good answer:
Common mistakes include forgetting spring-security-test, building MockMvc without Spring Security filters, using @WithMockUser with roles = "ROLE_ADMIN", forgetting CSRF for POST tests, expecting 401 when the user is actually authenticated but forbidden, testing manually created services instead of Spring beans, forgetting @EnableMethodSecurity, and using @WithMockUser when the code needs a custom principal.
65. Tiny Code Practice
Security rule:
.requestMatchers(HttpMethod.GET, "/api/tasks/**").hasAuthority("TASK_READ")
.requestMatchers(HttpMethod.POST, "/api/tasks/**").hasAuthority("TASK_WRITE")
.requestMatchers(HttpMethod.DELETE, "/api/tasks/**").hasRole("ADMIN")
Write tests:
@Test
void anonymousCannotReadTasks() throws Exception {
mockMvc.perform(get("/api/tasks"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(authorities = "TASK_READ")
void taskReaderCanReadTasks() throws Exception {
mockMvc.perform(get("/api/tasks"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(authorities = "TASK_READ")
void taskReaderCannotCreateTask() throws Exception {
mockMvc.perform(post("/api/tasks")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Task A"}
"""))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(authorities = "TASK_WRITE")
void taskWriterCanCreateTask() throws Exception {
mockMvc.perform(post("/api/tasks")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Task A"}
"""))
.andExpect(status().isCreated());
}
@Test
@WithMockUser(roles = "ADMIN")
void adminCanDeleteTask() throws Exception {
mockMvc.perform(delete("/api/tasks/1")
.with(csrf()))
.andExpect(status().isNoContent());
}
Questions:
- Which test checks 401?
- Which tests check 403?
- Which tests need CSRF?
- Why does admin use
roles = "ADMIN"? - Why does task reader use
authorities = "TASK_READ"?
Answers:
anonymousCannotReadTasks.taskReaderCannotCreateTask.- POST and DELETE tests if CSRF is enabled.
- Because
roles = "ADMIN"createsROLE_ADMIN. - Because the rule checks exact authority
TASK_READ.
66. Tiny Bug Practice 1
Problem:
@Test
@WithMockUser(authorities = "TASK_WRITE")
void createTask() throws Exception {
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Task A"}
"""))
.andExpect(status().isCreated());
}
Actual result:
403
Question:
What is likely wrong?
Answer:
CSRF token is missing. Add .with(csrf()) if CSRF is enabled.
67. Tiny Bug Practice 2
Problem:
@Test
@WithMockUser(roles = "ROLE_ADMIN")
void adminCanDelete() throws Exception {
mockMvc.perform(delete("/api/tasks/1").with(csrf()))
.andExpect(status().isNoContent());
}
Question:
What is wrong?
Answer:
roles should not include ROLE_. Use:
@WithMockUser(roles = "ADMIN")
or:
@WithMockUser(authorities = "ROLE_ADMIN")
68. Tiny Bug Practice 3
Problem:
TaskService taskService = new TaskService(taskRepository);
taskService.deleteTask(1L);
Question:
Why is this bad for method security testing?
Answer:
The service is manually created, not a Spring-managed bean. Method security proxy is bypassed. Autowire the service bean from Spring context.
69. Tiny Bug Practice 4
Problem:
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) {
}
Test shows USER can still call it.
Question:
What might be missing?
Answer:
@EnableMethodSecurity may be missing, or the method is not called through a Spring-managed proxy.
Practice Questions and Answers
Question 1
Which dependency provides Spring Security test support?
Answer:
spring-security-test.
Question 2
What does @WithMockUser do?
Answer:
It runs the test with a mocked authenticated user in the security context.
Question 3
What user does default @WithMockUser create?
Answer:
A user with username user, password password, and role USER, meaning authority ROLE_USER.
Question 4
What is the difference between roles and authorities in @WithMockUser?
Answer:
roles automatically adds the ROLE_ prefix. authorities uses exact authority strings.
Question 5
How do I test a 401 response?
Answer:
Make the request without authentication and expect status().isUnauthorized().
Question 6
How do I test a 403 response?
Answer:
Authenticate as a user without enough permission and expect status().isForbidden().
Question 7
How do I add CSRF token in MockMvc?
Answer:
Use .with(csrf()).
Question 8
When do POST tests need CSRF?
Answer:
When CSRF protection is enabled and the method is unsafe, such as POST, PUT, PATCH, or DELETE.
Question 9
What does httpBasic() test?
Answer:
It sends username/password through HTTP Basic authentication and can test the authentication flow.
Question 10
What is the difference between @WithMockUser and httpBasic()?
Answer:
@WithMockUser directly creates a mock authenticated user. httpBasic() sends credentials through the real Basic authentication flow.
Question 11
What does @WithUserDetails do?
Answer:
It loads a real user through UserDetailsService.
Question 12
How do I test a JWT resource server endpoint?
Answer:
Use MockMvc with .with(jwt()) and provide required authorities or claims.
Question 13
How do I test method security?
Answer:
Call the Spring-managed service bean with a mock authenticated user and assert success or AccessDeniedException.
Question 14
What exception should be expected when method security denies access?
Answer:
AccessDeniedException.
Question 15
Why might @WithMockUser not work with custom principal expressions?
Answer:
Because @WithMockUser creates a standard mock user, not my custom principal with fields like id or tenantId.
Question 16
How can I test with a custom principal in MockMvc?
Answer:
Use .with(user(customPrincipal)).
Question 17
What is @WithSecurityContext useful for?
Answer:
It is useful for creating custom security context annotations for tests, especially when a custom principal is needed.
Question 18
Why might MockMvc tests pass even though real security would block the request?
Answer:
Because MockMvc may be built without Spring Security filters, for example using standalone setup without applying springSecurity().
Question 19
Why is @WithMockUser(roles = "ROLE_ADMIN") wrong?
Answer:
Because roles adds ROLE_ automatically, so it may create ROLE_ROLE_ADMIN.
Question 20
Why should I test both allowed and denied cases?
Answer:
Because security must prove that correct users are allowed and incorrect users are blocked.
Final Memory Sentences
- Security tests prove access rules.
- Add
spring-security-test. - MockMvc can test secured endpoints.
- If MockMvc is built manually, apply
springSecurity(). @WithMockUsercreates a mock authenticated user.- Default
@WithMockUserhas roleUSER. roles = "ADMIN"createsROLE_ADMIN.authorities = "TASK_READ"creates exact authorityTASK_READ.- Test 401 with no authentication.
- Test 403 with authenticated user lacking permission.
- POST/PUT/PATCH/DELETE need
.with(csrf())if CSRF is enabled. permitAlldoes not automatically disable CSRF.httpBasic()tests Basic authentication flow.@WithMockUserdoes not test real password authentication.@WithUserDetailsuses realUserDetailsService.jwt()tests JWT resource server authorization.- Method security tests should call Spring-managed service beans.
- Denied method security usually throws
AccessDeniedException. @EnableMethodSecurityis required for@PreAuthorize.- Custom principals may need
.with(user(customPrincipal))or custom@WithSecurityContext. - Always test allowed and denied cases.