JUnit 5 Tests for JWT Auth APIs

Write JUnit 5 tests for role-based Task CRUD APIs secured with plain JWT authentication.

Introduction

30 Second Summary

You've built your API endpoints and added JWT security. But how do you prove the security rules actually work? One misconfigured role check can silently let anyone delete records they don't own.

In this project, you will write a JUnit 5 test suite that verifies role-based authorization and ownership rules for a Task CRUD API secured with plain JWT authentication. Your tests use Spring Security's @WithMockUser to simulate different roles without a running auth server.

What You'll Build

A test suite for a role-based Task CRUD API that verifies authentication enforcement, role-based create/assign permissions, and ownership-based status updates and deletions using MockMvc integration tests.

By the end of this project, you'll have:

  • AuthenticationTest.java proving that unauthenticated requests are rejected and authenticated users can access endpoints.
  • RoleAuthorizationTest.java verifying that ADMIN/MANAGER can assign tasks to any user while USER role auto-assigns to themselves.
  • OwnershipTest.java confirming that only the task owner can change status or delete, and non-owners get 403 Forbidden.
  • Secret Mission: A custom @WithMockTaskOwner annotation that pre-populates the SecurityContext with a username matching a task's assignee.

Are there any prerequisites?

This project assumes you have an existing Spring Boot project with JWT authentication using the JJWT library (io.jsonwebtoken) and a custom JwtAuthenticationFilter. You should have Java 17+, Maven, and an IDE (Eclipse or IntelliJ) installed.

The tests target a security configuration, task controller, and task service that you will create (or verify) in Step 1. No external authorization server or database is needed.

Before We Start

Your Spring Boot API uses JWT authentication to protect its endpoints. But how do you know the security rules actually work?

In this step, you will add the test dependency that powers Spring Security's testing utilities, then set up the controller and security configuration that your test suite will target.

In this step, get ready to:
  • Add the spring-security-test dependency to your project.
  • Set up a SecurityConfig and UserController with protected endpoints.
  • Confirm the test infrastructure compiles and runs cleanly.
Add the spring-security-test dependency

Spring Security provides a dedicated test module that includes the jwt() RequestPostProcessor. This lets you simulate authenticated requests without spinning up a real authorization server.

  • In Eclipse, open your project's pom.xml file.
  • Find the <dependencies> section and add the following dependency:
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>

Why is there no version tag?

Spring Boot's BOM (Bill of Materials) manages the version of spring-security-test automatically. It ensures the test module version matches your project's Spring Security version without you specifying it explicitly.

  • Save pom.xml.
  • Verify the project compiles by running this command in your terminal:
mvn compile
  • You should see BUILD SUCCESS in the terminal output.

Seeing a BUILD FAILURE?

  • Check that the dependency XML is inside the <dependencies> block, not inside <dependencyManagement>.
  • Make sure there is no <version> tag on the dependency. Spring Boot's parent POM handles versioning.
  • Confirm your internet connection is active so Maven can download the artifact.

Why won't my project compile after adding spring-security-test?

Set up the security configuration and controller

Your tests need endpoints to hit. You will create (or verify you already have) a SecurityConfig class that defines the authorization rules, and a UserController with public, authenticated, and scope-protected endpoints.

  • Create the file src/main/java/com/example/demo/config/SecurityConfig.java (or open it if it already exists) and paste the following code:
package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
                .requestMatchers(HttpMethod.POST, "/api/users").hasAuthority("SCOPE_write")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(Customizer.withDefaults())
            );
        return http.build();
    }
}

What does this configuration do?

  • CSRF is disabled because stateless JWT APIs do not use cookies, so CSRF protection is unnecessary.
  • Requests to /api/public/** are open to everyone without authentication.
  • Requests to /api/admin/** require the SCOPE_admin authority.
  • POST requests to /api/users require the SCOPE_write authority.
  • All other requests require a valid JWT (authenticated).
  • Save SecurityConfig.java.
  • Now create the file src/main/java/com/example/demo/controller/UserController.java (or open it if it already exists) and paste the following code:
package com.example.demo.controller;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class UserController {

    @GetMapping("/public/health")
    public ResponseEntity<Map<String, String>> health() {
        return ResponseEntity.ok(Map.of("status", "UP"));
    }

    @GetMapping("/users/me")
    public ResponseEntity<Map<String, Object>> getCurrentUser(@AuthenticationPrincipal Jwt jwt) {
        Map<String, Object> userInfo = new HashMap<>();
        userInfo.put("subject", jwt.getSubject());
        userInfo.put("email", jwt.getClaimAsString("email"));
        return ResponseEntity.ok(userInfo);
    }

    @GetMapping("/admin/users")
    public ResponseEntity<List<Map<String, String>>> getAllUsers() {
        return ResponseEntity.ok(List.of(
            Map.of("username", "alice"),
            Map.of("username", "bob")
        ));
    }

    @PostMapping("/users")
    public ResponseEntity<Map<String, String>> createUser(@RequestBody Map<String, String> user) {
        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    }
}

What do these endpoints do?

  • GET /api/public/health is a public health check that returns a status of UP.
  • GET /api/users/me extracts the subject and email from the JWT and returns them. This is the endpoint your authentication tests will target.
  • GET /api/admin/users returns a hardcoded user list. Only callers with SCOPE_admin can access it.
  • POST /api/users creates a user. Only callers with SCOPE_write can access it.
  • Save UserController.java.

Seeing import errors in Eclipse?

  • Make sure your project already has spring-boot-starter-oauth2-resource-server in pom.xml. This provides the Jwt class and OAuth2 annotations.
  • Try right-clicking the project in Eclipse, then selecting Maven followed by Update Project to refresh dependencies.

Why am I seeing unresolved imports in my Spring Security project?

✔️ Awesome, I've got everything!

Great. Double check you have saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the complete SecurityConfig.java:

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
                .requestMatchers(HttpMethod.POST, "/api/users").hasAuthority("SCOPE_write")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(Customizer.withDefaults())
            );
        return http.build();
    }
}

Here is the complete UserController.java:

package com.example.demo.controller;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class UserController {

    @GetMapping("/public/health")
    public ResponseEntity<Map<String, String>> health() {
        return ResponseEntity.ok(Map.of("status", "UP"));
    }

    @GetMapping("/users/me")
    public ResponseEntity<Map<String, Object>> getCurrentUser(@AuthenticationPrincipal Jwt jwt) {
        Map<String, Object> userInfo = new HashMap<>();
        userInfo.put("subject", jwt.getSubject());
        userInfo.put("email", jwt.getClaimAsString("email"));
        return ResponseEntity.ok(userInfo);
    }

    @GetMapping("/admin/users")
    public ResponseEntity<List<Map<String, String>>> getAllUsers() {
        return ResponseEntity.ok(List.of(
            Map.of("username", "alice"),
            Map.of("username", "bob")
        ));
    }

    @PostMapping("/users")
    public ResponseEntity<Map<String, String>> createUser(@RequestBody Map<String, String> user) {
        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    }
}
Confirm the test infrastructure works

Before writing any test classes, confirm that Maven's test lifecycle runs cleanly with no failures.

  • Run the full test suite by executing this command:
mvn test
  • You should see BUILD SUCCESS with zero test failures in the output.

What does an empty test run prove?

A clean mvn test with no failures confirms that your project structure, dependencies, and Spring Boot application context all load correctly. If something is misconfigured (missing dependency, broken config class, bad annotation), it would surface here before you write any test logic.

Tests failing before you wrote any?

  • If you see an error about the JwtDecoder bean, make sure your application.properties (or application.yml) has a valid spring.security.oauth2.resourceserver.jwt.issuer-uri or jwk-set-uri configured. The tests you write in the next steps will mock this bean, but the default application context may still require it.
  • If the context fails to start, verify that SecurityConfig.java and UserController.java are in the correct packages under src/main/java.

Why is my Spring Boot test context failing to start?

Your test infrastructure is ready. Next up, you will write your first real test class that proves unauthenticated requests get rejected and authenticated requests succeed.

Set Up SecurityConfig and UserController

Set Up SecurityConfig and TaskController

Before writing any test, you need a clear picture of the code your tests will target. Your test suite will fire requests at task endpoints and verify that the security configuration enforces role-based and ownership rules correctly.

In this step, you will walk through the SecurityConfig, TaskController, and TaskService classes in your project. Understanding what each security rule and endpoint does is essential for writing meaningful tests against them.

In this step, get ready to:
  • Review the security rules in SecurityConfig.java.
  • Review the task endpoints in TaskController.java.
  • Confirm the test infrastructure compiles and runs with mvn test.
Review your security configuration

The SecurityConfig class defines which endpoints are public, which require authentication, and which require specific roles. Every test you write in later steps will assert that these rules are enforced correctly.

  • In your IDE's project explorer, navigate to src/main/java/com/example/demo/config/.
  • Open SecurityConfig.java and verify it matches the following code:
package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter) {
        this.jwtAuthFilter = jwtAuthFilter;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // Disable CSRF since this is a stateless JWT API
            .csrf(AbstractHttpConfigurer::disable)
            // Stateless session - no server-side session
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .authorizeHttpRequests(auth -> auth
                // Public endpoint - no auth required
                .requestMatchers("/api/public/**").permitAll()
                // All task endpoints require authentication
                .requestMatchers("/api/tasks/**").authenticated()
                // Everything else requires authentication
                .anyRequest().authenticated()
            )
            // Add JWT filter before Spring's default auth filter
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

What do these security rules do?

  • .csrf(AbstractHttpConfigurer::disable) disables CSRF because stateless JWT APIs do not use browser cookies.
  • SessionCreationPolicy.STATELESS tells Spring Security to never create an HTTP session. Each request must carry its own JWT.
  • .requestMatchers("/api/public/**").permitAll() allows anyone to hit the health endpoint without a token.
  • .requestMatchers("/api/tasks/**").authenticated() requires a valid JWT for all task endpoints. Role checks happen inside the controller via @PreAuthorize.
Review the task controller

The TaskController defines the CRUD endpoints that the security rules protect. Role-based authorization is handled at the method level using @PreAuthorize annotations. Ownership checks happen in the service layer.

  • Navigate to src/main/java/com/example/demo/controller/.
  • Open TaskController.java and verify it matches the following code:
package com.example.demo.controller;

import java.security.Principal;
import java.util.List;
import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.model.Task;
import com.example.demo.service.TaskService;

@RestController
@RequestMapping("/api/tasks")
public class TaskController {

    private final TaskService taskService;

    public TaskController(TaskService taskService) {
        this.taskService = taskService;
    }

    // Any authenticated user can list tasks
    @GetMapping
    public ResponseEntity<List<Task>> getAllTasks() {
        return ResponseEntity.ok(taskService.getAllTasks());
    }

    // Any authenticated user can view a task
    @GetMapping("/{id}")
    public ResponseEntity<Task> getTask(@PathVariable Long id) {
        return taskService.getTask(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    // ADMIN and MANAGER can assign to any user; USER auto-assigns to self
    @PostMapping
    @PreAuthorize("isAuthenticated()")
    public ResponseEntity<Task> createTask(@RequestBody Map<String, String> body,
                                           Principal principal) {
        String title = body.get("title");
        String assignee = body.get("assignee");
        Task task = taskService.createTask(title, assignee, principal);
        return ResponseEntity.status(HttpStatus.CREATED).body(task);
    }

    // Only the task owner can change status
    @PatchMapping("/{id}/status")
    @PreAuthorize("isAuthenticated()")
    public ResponseEntity<Task> updateStatus(@PathVariable Long id,
                                             @RequestBody Map<String, String> body,
                                             Principal principal) {
        String newStatus = body.get("status");
        return taskService.updateStatus(id, newStatus, principal)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.status(HttpStatus.FORBIDDEN).build());
    }

    // Only the task owner can delete
    @DeleteMapping("/{id}")
    @PreAuthorize("isAuthenticated()")
    public ResponseEntity<Void> deleteTask(@PathVariable Long id, Principal principal) {
        boolean deleted = taskService.deleteTask(id, principal);
        return deleted
            ? ResponseEntity.noContent().build()
            : ResponseEntity.status(HttpStatus.FORBIDDEN).build();
    }
}

What does each endpoint do?

  • GET /api/tasks returns all tasks. Any authenticated user can call this.
  • POST /api/tasks creates a task. ADMIN/MANAGER can set any assignee; USER role auto-assigns to themselves via the service layer.
  • PATCH /api/tasks/{id}/status changes a task's status. The service checks ownership and returns empty if the caller is not the task owner.
  • DELETE /api/tasks/{id} deletes a task. Only the task's assignee (owner) can delete it.
Review the task service

The TaskService contains the business logic for role-based assignment and ownership checks. It uses an in-memory list so the project stays focused on testing without needing a database.

  • Navigate to src/main/java/com/example/demo/service/.
  • Open TaskService.java and verify it matches the following code:
package com.example.demo.service;

import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service;

import com.example.demo.model.Task;

@Service
public class TaskService {

    private final List<Task> tasks = new ArrayList<>();
    private final AtomicLong idCounter = new AtomicLong(1);

    public List<Task> getAllTasks() {
        return List.copyOf(tasks);
    }

    public Optional<Task> getTask(Long id) {
        return tasks.stream().filter(t -> t.getId().equals(id)).findFirst();
    }

    public Task createTask(String title, String assignee, Principal principal) {
        Authentication auth = (Authentication) principal;
        String caller = auth.getName();

        // Check if caller has ADMIN or MANAGER role
        boolean canAssignOthers = auth.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .anyMatch(a -> a.equals("ROLE_ADMIN") || a.equals("ROLE_MANAGER"));

        // USER role auto-assigns to self
        String effectiveAssignee = canAssignOthers && assignee != null ? assignee : caller;

        Task task = new Task(idCounter.getAndIncrement(), title, effectiveAssignee, "TODO");
        tasks.add(task);
        return task;
    }

    public Optional<Task> updateStatus(Long id, String newStatus, Principal principal) {
        return getTask(id)
            .filter(task -> task.getAssignee().equals(principal.getName()))
            .map(task -> {
                task.setStatus(newStatus);
                return task;
            });
    }

    public boolean deleteTask(Long id, Principal principal) {
        return tasks.removeIf(task ->
            task.getId().equals(id) && task.getAssignee().equals(principal.getName()));
    }
}

How does the role-based assignment work?

The createTask method casts the Principal to Authentication to read the caller's granted authorities. If the caller has ROLE_ADMIN or ROLE_MANAGER, they can assign the task to any user.

Users without those roles get the task auto-assigned to their own username. The ownership checks in updateStatus and deleteTask compare the task's assignee against principal.getName().

Confirm the test infrastructure works
  • Confirm the test infrastructure works by running this command in your terminal:
mvn test

You should see BUILD SUCCESS at the bottom of the output.

Seeing BUILD FAILURE?

  • Check that your pom.xml has the spring-security-test dependency. The version is managed by the Spring Boot BOM.
  • Make sure your SecurityConfig.java package declaration matches com.example.demo.config and your TaskController.java matches com.example.demo.controller.
  • Verify you have the JJWT dependencies (jjwt-api, jjwt-impl, jjwt-jackson) at version 0.12.6 in your pom.xml.

Help me troubleshoot my mvn test failure for my Spring Boot JWT project.

Your security configuration, task controller, and service layer are in place and verified. Next up, you will write your first authentication tests to prove that unauthenticated requests are rejected and authenticated users can access task endpoints.

Write Authentication Tests

Your project is set up with the security configuration, task controller, and service layer ready to test. Now it's time to prove those security rules actually work by writing integration tests.

In this step, you'll create AuthenticationTest.java to verify two fundamental behaviors: unauthenticated requests are rejected, and requests from authenticated users (simulated with @WithMockUser) are accepted. These tests use MockMvc to simulate HTTP requests without needing a running server.

In this step, get ready to:
  • Create AuthenticationTest.java with unauthenticated access tests.
  • Add authenticated request tests using @WithMockUser.
  • Run the test class and confirm all tests pass.
Create the test class with unauthenticated access tests

Integration tests for a secured API need to boot the full application context so the real security filter chain runs. The @SpringBootTest annotation handles that, and @AutoConfigureMockMvc gives you a pre-configured MockMvc instance to send requests through the filter chain.

  • Create a new file called AuthenticationTest.java in src/test/java/com/example/demo/.
  • Paste the following code into the file:
package com.example.demo;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
@DisplayName("Authentication Integration Tests")
class AuthenticationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @DisplayName("Task list returns 401 without authentication")
    void taskList_noAuth_returns401() throws Exception {
        // Send a GET request with no credentials to the tasks endpoint
        mockMvc.perform(get("/api/tasks"))
            .andExpect(status().isUnauthorized());
    }

    @Test
    @DisplayName("Task creation returns 401 without authentication")
    void createTask_noAuth_returns401() throws Exception {
        // POST with no credentials is rejected
        mockMvc.perform(get("/api/tasks/1"))
            .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(username = "alice", roles = {"USER"})
    @DisplayName("Authenticated user can list tasks")
    void taskList_withAuth_returns200() throws Exception {
        // @WithMockUser simulates an authenticated user in the SecurityContext
        mockMvc.perform(get("/api/tasks"))
            .andExpect(status().isOk());
    }

    @Test
    @WithMockUser(username = "alice", roles = {"ADMIN"})
    @DisplayName("Authenticated admin can list tasks")
    void taskList_withAdmin_returns200() throws Exception {
        // Admin role also gets access to the tasks list
        mockMvc.perform(get("/api/tasks"))
            .andExpect(status().isOk());
    }
}

What does this code do?

This test class boots your full Spring application and sends HTTP requests through the real security filter chain.

  • @SpringBootTest loads the entire application context, including your SecurityConfig and JwtAuthenticationFilter.
  • @AutoConfigureMockMvc creates a MockMvc instance wired through the security filter chain.
  • @WithMockUser populates the SecurityContext with a mock authentication before the test runs. The JWT filter is bypassed because the context is already authenticated.
  • The unauthenticated tests confirm that requests without any credentials are rejected with a 401 status.

Why @WithMockUser instead of a real JWT?

@WithMockUser pre-populates the SecurityContext before the request reaches your filter chain. This is faster and simpler than generating a real signed JWT for every test.

Your JwtAuthenticationFilter only runs when the SecurityContext is empty. When @WithMockUser has already set it, the filter skips token parsing. This lets you test authorization rules independently from token validation.

  • Save AuthenticationTest.java.

Seeing import errors?

  • Confirm the spring-security-test dependency is in your pom.xml. It provides the @WithMockUser annotation.
  • If your IDE shows red underlines, right-click the project and select Maven then Update Project to re-sync dependencies.

Help me fix import errors in my Spring Security test class.

Run the authentication tests
  • Run the authentication tests by executing this command in your terminal:
mvn test -Dtest=AuthenticationTest

You should see output showing 4 tests run with 0 failures, followed by BUILD SUCCESS.

Seeing test failures?

  • If a test expecting 401 gets 200 instead, check your SecurityConfig.java has .requestMatchers("/api/tasks/**").authenticated().
  • If a test expecting 200 gets 401, make sure @WithMockUser is on the test method. Without it, MockMvc sends no credentials.
  • If you see a Spring context startup failure, check that your JwtAuthenticationFilter is annotated with @Component so Spring can inject it into the security config.

Help me debug my failing authentication tests.

Four tests passing means your security rules are proven: task endpoints demand authentication, and @WithMockUser correctly simulates authenticated users. Next up, you'll test the role-based authorization layer to prove that different roles get different task creation behavior.

Write Authorization Tests

Your authentication tests confirm that protected endpoints reject unauthenticated requests. But being authenticated is only half the story.

Authorization determines what an authenticated user is allowed to do. Your SecurityConfig restricts certain endpoints to users with specific scopes. In this step, you'll write tests that prove those scope-based rules work correctly.

In this step, get ready to:
  • Create AuthorizationTest.java with @Nested test classes for admin and write endpoints.
  • Test that insufficient scopes result in 403 Forbidden responses.
  • Test that correct scopes grant access to protected resources.
Create AuthorizationTest.java with negative authorization tests

Authentication tests answer "can this person get in?" while authorization tests answer "is this person allowed to do this specific thing?" The .authorities() method on the jwt() post-processor lets you assign specific scopes to a simulated JWT.

  • In Eclipse, create a new file called AuthorizationTest.java in src/test/java/com/example/demo/.
  • Paste the following code into the file:
package com.example.demo;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockbean.MockBean;
import org.springframework.http.MediaType;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
@DisplayName("Authorization Integration Tests")
class AuthorizationTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private JwtDecoder jwtDecoder;

    @Nested
    @DisplayName("Admin endpoint access")
    class AdminEndpointTests {

        @Test
        @DisplayName("Returns 403 when user lacks admin scope")
        void adminEndpoint_withoutAdminScope_returns403() throws Exception {
            // Request /api/admin/users with only SCOPE_read authority
            mockMvc.perform(get("/api/admin/users")
                    .with(jwt().authorities(new SimpleGrantedAuthority("SCOPE_read"))))
                .andExpect(status().isForbidden());
        }
    }

    @Nested
    @DisplayName("Write endpoint access")
    class WriteEndpointTests {

        @Test
        @DisplayName("Returns 403 when user lacks write scope")
        void createUser_withoutWriteScope_returns403() throws Exception {
            // POST to /api/users with only SCOPE_read authority
            mockMvc.perform(post("/api/users")
                    .with(jwt().authorities(new SimpleGrantedAuthority("SCOPE_read")))
                    .contentType(MediaType.APPLICATION_JSON)
                    .content("{\"username\":\"charlie\"}"))
                .andExpect(status().isForbidden());
        }
    }
}

What does this code do?

This test class uses a different pattern than your authentication tests. Instead of one flat list of tests, it uses @Nested inner classes to group related tests together.

  • AdminEndpointTests groups all tests for the /api/admin/** endpoints, which require SCOPE_admin.
  • WriteEndpointTests groups tests for POST /api/users, which requires SCOPE_write.
  • The .authorities() method replaces the default authorities on the JWT. By giving the user only SCOPE_read, you prove that SecurityConfig rejects requests with insufficient scopes.

Why @Nested classes?

JUnit 5's @Nested annotation lets you organize tests into logical groups inside one file. When you run the suite, the output shows a tree structure, making it easy to see which endpoint group has a failing test.

  • Save AuthorizationTest.java.

Seeing import errors in Eclipse?

If Eclipse shows red underlines on the imports, right-click the project and select Maven then Update Project. This re-syncs your classpath with the pom.xml dependencies.

Help me fix import errors in my Spring Security test class.

Add positive authorization tests

Negative tests prove the gate is closed. Positive tests prove it opens for the right credentials. You need both to confirm your security rules work in both directions.

  • In AuthorizationTest.java, find the AdminEndpointTests nested class. Add this test method below the existing adminEndpoint_withoutAdminScope_returns403 method:
        @Test
        @DisplayName("Returns 200 when user has admin scope")
        void adminEndpoint_withAdminScope_returns200() throws Exception {
            // Request with SCOPE_admin authority grants access
            mockMvc.perform(get("/api/admin/users")
                    .with(jwt().authorities(new SimpleGrantedAuthority("SCOPE_admin"))))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].username").value("alice"));
        }

What does this test verify?

This test gives the JWT the SCOPE_admin authority and confirms the endpoint returns 200 with the expected data. The jsonPath("$[0].username").value("alice") assertion proves the response body is actually returned, not just an empty 200.

  • Now find the WriteEndpointTests nested class. Add these two test methods below the existing createUser_withoutWriteScope_returns403 method:
        @Test
        @DisplayName("Returns 201 when user has write scope")
        void createUser_withWriteScope_returns201() throws Exception {
            // POST with SCOPE_write authority creates the user
            mockMvc.perform(post("/api/users")
                    .with(jwt().authorities(new SimpleGrantedAuthority("SCOPE_write")))
                    .contentType(MediaType.APPLICATION_JSON)
                    .content("{\"username\":\"charlie\"}"))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.username").value("charlie"));
        }

        @Test
        @DisplayName("Returns 201 when user has multiple scopes including write")
        void createUser_withMultipleScopes_returns201() throws Exception {
            // Multiple authorities including SCOPE_write still grants access
            mockMvc.perform(post("/api/users")
                    .with(jwt().authorities(
                        new SimpleGrantedAuthority("SCOPE_read"),
                        new SimpleGrantedAuthority("SCOPE_write")
                    ))
                    .contentType(MediaType.APPLICATION_JSON)
                    .content("{\"username\":\"charlie\"}"))
                .andExpect(status().isCreated());
        }

What do these tests verify?

  • createUser_withWriteScope_returns201 proves that a single SCOPE_write authority is enough to create a user. It also checks the response body to confirm the endpoint processed the request.
  • createUser_withMultipleScopes_returns201 proves that having extra scopes (like SCOPE_read alongside SCOPE_write) does not interfere with authorization. Real users often carry multiple scopes.
  • Save AuthorizationTest.java.

Seeing a compilation error?

Make sure the new test methods are inside the correct nested class. The admin test goes inside AdminEndpointTests and the write tests go inside WriteEndpointTests. Each nested class has its own closing brace.

Help me fix my nested test class structure.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before running the tests.

ⓧ I'd like to double check the full code

package com.example.demo;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockbean.MockBean;
import org.springframework.http.MediaType;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
@DisplayName("Authorization Integration Tests")
class AuthorizationTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private JwtDecoder jwtDecoder;

    @Nested
    @DisplayName("Admin endpoint access")
    class AdminEndpointTests {

        @Test
        @DisplayName("Returns 403 when user lacks admin scope")
        void adminEndpoint_withoutAdminScope_returns403() throws Exception {
            mockMvc.perform(get("/api/admin/users")
                    .with(jwt().authorities(new SimpleGrantedAuthority("SCOPE_read"))))
                .andExpect(status().isForbidden());
        }

        @Test
        @DisplayName("Returns 200 when user has admin scope")
        void adminEndpoint_withAdminScope_returns200() throws Exception {
            mockMvc.perform(get("/api/admin/users")
                    .with(jwt().authorities(new SimpleGrantedAuthority("SCOPE_admin"))))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].username").value("alice"));
        }
    }

    @Nested
    @DisplayName("Write endpoint access")
    class WriteEndpointTests {

        @Test
        @DisplayName("Returns 403 when user lacks write scope")
        void createUser_withoutWriteScope_returns403() throws Exception {
            mockMvc.perform(post("/api/users")
                    .with(jwt().authorities(new SimpleGrantedAuthority("SCOPE_read")))
                    .contentType(MediaType.APPLICATION_JSON)
                    .content("{\"username\":\"charlie\"}"))
                .andExpect(status().isForbidden());
        }

        @Test
        @DisplayName("Returns 201 when user has write scope")
        void createUser_withWriteScope_returns201() throws Exception {
            mockMvc.perform(post("/api/users")
                    .with(jwt().authorities(new SimpleGrantedAuthority("SCOPE_write")))
                    .contentType(MediaType.APPLICATION_JSON)
                    .content("{\"username\":\"charlie\"}"))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.username").value("charlie"));
        }

        @Test
        @DisplayName("Returns 201 when user has multiple scopes including write")
        void createUser_withMultipleScopes_returns201() throws Exception {
            mockMvc.perform(post("/api/users")
                    .with(jwt().authorities(
                        new SimpleGrantedAuthority("SCOPE_read"),
                        new SimpleGrantedAuthority("SCOPE_write")
                    ))
                    .contentType(MediaType.APPLICATION_JSON)
                    .content("{\"username\":\"charlie\"}"))
                .andExpect(status().isCreated());
        }
    }
}
Run the authorization tests
  • Run the authorization test class by executing this command in your terminal:
mvn test -Dtest=AuthorizationTest

You should see all 5 tests pass. The output shows the nested structure with AdminEndpointTests and WriteEndpointTests grouped separately.

Tests failing with 401 instead of 403?

A 401 means the JWT was not recognized at all. Check that your test class has @MockBean private JwtDecoder jwtDecoder; declared at the class level. Without this mock, the application context fails to create a JwtDecoder bean and JWT processing breaks.

Help me debug why my authorization tests return 401 instead of 403.

Your authorization tests now prove that scope-based access control works in both directions. Next up, you'll test that custom JWT claims are correctly extracted and returned by your controller.

Write JWT Claims Tests

Your authorization tests prove that scope-based access control works. The next layer to test is whether your controller can actually read the data inside a JWT.

Real-world APIs extract claims like email addresses, user IDs, and issuer URLs from tokens to personalize responses. In this step, you will write tests that configure JWTs with specific claims and verify the controller returns them correctly.

In this step, get ready to:
  • Write tests verifying custom JWT claims are accessible in controller methods.
  • Test issuer and temporal claims to validate the full range of the JWT builder.
  • Run the complete test suite across all three test classes.
Create JwtClaimsTest.java with custom claims tests

The UserController uses @AuthenticationPrincipal Jwt to extract the subject and email from the token. You need to prove that whatever claims you put into the JWT actually arrive in the controller.

  • Create a new file called JwtClaimsTest.java in src/test/java/com/example/demo/.
  • Paste in the following class with the first test method:
package com.example.demo;

import java.time.Instant;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockbean.MockBean;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
@DisplayName("JWT Claims Validation Tests")
class JwtClaimsTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private JwtDecoder jwtDecoder;

    @Test
    @DisplayName("Controller receives custom claims from JWT")
    void customClaims_areAccessibleInController() throws Exception {
        mockMvc.perform(get("/api/users/me")
                .with(jwt().jwt(token -> token
                    .subject("user-123")
                    .claim("email", "alice@example.com")
                )))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.subject").value("user-123"))
            .andExpect(jsonPath("$.email").value("alice@example.com"));
    }
}

What does this code do?

This test uses the same jwt() post-processor pattern from your earlier tests, but now you are asserting on the response body rather than just the HTTP status.

  • .subject("user-123") sets the sub claim, which the controller reads via jwt.getSubject().
  • .claim("email", "alice@example.com") adds a custom claim, which the controller reads via jwt.getClaimAsString("email").
  • The jsonPath assertions verify that the values configured in the test token arrive in the JSON response.
  • Save JwtClaimsTest.java.
  • Confirm the single test passes by running:
mvn test -Dtest=JwtClaimsTest

You should see 1 test pass with a green BUILD SUCCESS message.

Seeing a compilation error?

Check that your package declaration is package com.example.demo; and matches the folder structure. Also verify the static imports for jwt, get, jsonPath, and status are all present.

Help me debug a compilation error in JwtClaimsTest

Add issuer and temporal claims tests

The jwt() builder supports the full range of standard JWT claims beyond subject and email. Testing these proves your security infrastructure handles real-world tokens that carry issuer URLs, timestamps, and multiple custom fields.

  • In JwtClaimsTest.java, find the closing brace of the customClaims_areAccessibleInController method.
  • Add the following three test methods below it:
    @Test
    @DisplayName("JWT with issuer claim is processed correctly")
    void issuerClaim_isProcessed() throws Exception {
        mockMvc.perform(get("/api/users/me")
                .with(jwt().jwt(token -> token
                    .subject("user-456")
                    .claim("iss", "https://auth.example.com")
                    .claim("email", "bob@example.com")
                )))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.subject").value("user-456"))
            .andExpect(jsonPath("$.email").value("bob@example.com"));
    }

    @Test
    @DisplayName("JWT with temporal claims is valid")
    void temporalClaims_areProcessed() throws Exception {
        Instant now = Instant.now();
        mockMvc.perform(get("/api/users/me")
                .with(jwt().jwt(token -> token
                    .subject("user-789")
                    .issuedAt(now)
                    .expiresAt(now.plusSeconds(3600))
                    .claim("email", "carol@example.com")
                )))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.subject").value("user-789"))
            .andExpect(jsonPath("$.email").value("carol@example.com"));
    }

    @Test
    @DisplayName("Different subjects produce different responses")
    void differentSubjects_produceDifferentResponses() throws Exception {
        mockMvc.perform(get("/api/users/me")
                .with(jwt().jwt(token -> token
                    .subject("admin-001")
                    .claim("email", "admin@example.com")
                )))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.subject").value("admin-001"))
            .andExpect(jsonPath("$.email").value("admin@example.com"));
    }

What do these tests cover?

  • issuerClaim_isProcessed adds an iss claim (the authorization server URL). This proves the token builder handles standard registered claims alongside custom ones.
  • temporalClaims_areProcessed uses .issuedAt() and .expiresAt() to attach time-based claims. These are critical in production tokens and the test confirms they do not break authentication.
  • differentSubjects_produceDifferentResponses confirms that the controller reads from the actual token and not a cached or default value. Each unique subject produces a unique response.
  • Save JwtClaimsTest.java.
  • Run the claims tests by running:
mvn test -Dtest=JwtClaimsTest

You should see 4 tests pass with BUILD SUCCESS.

Seeing a failure on the temporal claims test?

Make sure you declared Instant now = Instant.now(); as a local variable inside the test method, not as a class field. Also confirm the import java.time.Instant; is present at the top of the file.

Help me debug my temporal claims test

Run the full test suite

With all three test classes complete, you need to verify they run together without conflicts. Tests that pass individually can sometimes fail when the full application context is shared.

  • Run the entire test suite by running:
mvn test

You should see 13 tests pass across all three test classes (4 in AuthenticationTest, 5 in AuthorizationTest, 4 in JwtClaimsTest) with BUILD SUCCESS.

Why run all tests together?

Each test class uses @SpringBootTest which loads the full application context. Running all classes together verifies that your security configuration, controller logic, and mock setup do not interfere with each other across shared context.

If the full suite runs in under 5 seconds, your test infrastructure is healthy. Slow tests often indicate missing mocks or unnecessary external calls.

Some tests pass individually but fail together?

This usually means a shared application context issue. Confirm each test class has its own @MockBean JwtDecoder declaration. Without it, Spring Boot attempts to contact the authorization server and fails.

Help me debug tests that fail when running together

✔️ Awesome, I've got everything!

Great. Double check you have saved JwtClaimsTest.java and your full suite passes.

ⓧ I'd like to double check the full code

package com.example.demo;

import java.time.Instant;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockbean.MockBean;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
@DisplayName("JWT Claims Validation Tests")
class JwtClaimsTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private JwtDecoder jwtDecoder;

    @Test
    @DisplayName("Controller receives custom claims from JWT")
    void customClaims_areAccessibleInController() throws Exception {
        mockMvc.perform(get("/api/users/me")
                .with(jwt().jwt(token -> token
                    .subject("user-123")
                    .claim("email", "alice@example.com")
                )))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.subject").value("user-123"))
            .andExpect(jsonPath("$.email").value("alice@example.com"));
    }

    @Test
    @DisplayName("JWT with issuer claim is processed correctly")
    void issuerClaim_isProcessed() throws Exception {
        mockMvc.perform(get("/api/users/me")
                .with(jwt().jwt(token -> token
                    .subject("user-456")
                    .claim("iss", "https://auth.example.com")
                    .claim("email", "bob@example.com")
                )))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.subject").value("user-456"))
            .andExpect(jsonPath("$.email").value("bob@example.com"));
    }

    @Test
    @DisplayName("JWT with temporal claims is valid")
    void temporalClaims_areProcessed() throws Exception {
        Instant now = Instant.now();
        mockMvc.perform(get("/api/users/me")
                .with(jwt().jwt(token -> token
                    .subject("user-789")
                    .issuedAt(now)
                    .expiresAt(now.plusSeconds(3600))
                    .claim("email", "carol@example.com")
                )))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.subject").value("user-789"))
            .andExpect(jsonPath("$.email").value("carol@example.com"));
    }

    @Test
    @DisplayName("Different subjects produce different responses")
    void differentSubjects_produceDifferentResponses() throws Exception {
        mockMvc.perform(get("/api/users/me")
                .with(jwt().jwt(token -> token
                    .subject("admin-001")
                    .claim("email", "admin@example.com")
                )))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.subject").value("admin-001"))
            .andExpect(jsonPath("$.email").value("admin@example.com"));
    }
}

Your test suite now covers all three layers of JWT security: authentication enforcement, authorization scopes, and claims extraction. Next up, you will take your testing patterns to the next level with a custom annotation that eliminates repetitive setup.

Secret mission

Create a Custom @WithMockJwt Annotation

Your test suite works, but every test method repeats the same jwt() post-processor setup. What if you could replace all of that boilerplate with a single annotation? Build a custom @WithMockJwt annotation backed by Spring Security's WithSecurityContextFactory to pre-configure JWT authentication declaratively.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally using Maven and the Spring Boot test framework. There are no ongoing costs or external services to manage.

Resources you used:

  • spring-security-test dependency added to pom.xml.
  • Test files: AuthenticationTest.java, AuthorizationTest.java, JwtClaimsTest.java, CustomAnnotationTest.java.
  • Custom annotation files: WithMockJwt.java and WithMockJwtSecurityContextFactory.java in src/test/java/com/example/demo/annotation/.
  • SecurityConfig.java and UserController.java in src/main/java/com/example/demo/.

Keep everything running

No action needed. Choose this if you plan to continue developing your API or want the test suite available for CI/CD pipelines.

  • Your test files remain in src/test/java/ and run automatically with every mvn test invocation.
  • The @WithMockJwt annotation is reusable across any future test class in this project.
  • No external services or accounts were created, so there is nothing consuming resources.

Pause - I'll come back to this later

Since everything runs locally with no background processes, there is nothing to pause. Your test files sit in the project directory and consume no resources until you run them.

  • Close Eclipse if you want to free up memory.
  • Your project will be right where you left it when you reopen Eclipse later.

Delete - I don't want to use this again

Remove the test files and dependency you added during this project. This restores your project to its pre-test state.

  • Delete the test files by running this from your project root:
rm src/test/java/com/example/demo/AuthenticationTest.java
rm src/test/java/com/example/demo/AuthorizationTest.java
rm src/test/java/com/example/demo/JwtClaimsTest.java
rm src/test/java/com/example/demo/CustomAnnotationTest.java
rm -rf src/test/java/com/example/demo/annotation/
  • Open pom.xml and remove the spring-security-test dependency block.
  • If you created SecurityConfig.java and UserController.java specifically for this project (rather than verifying existing ones), delete those as well:
rm src/main/java/com/example/demo/config/SecurityConfig.java
rm src/main/java/com/example/demo/controller/UserController.java
  • Run mvn compile to confirm your project still compiles cleanly after removing the files.

Nice Work!

Nice Work!

Nice work! You've just built a comprehensive JUnit 5 test suite that proves your JWT authentication and authorization rules are correct without needing a running authorization server.

You've learned how to:

  • Built AuthenticationTest.java with MockMvc integration tests proving public endpoints are open and protected endpoints require valid JWTs.
  • Built AuthorizationTest.java with @Nested test classes proving scope-based access control is enforced with the jwt() RequestPostProcessor and .authorities() method.
  • Built JwtClaimsTest.java proving custom JWT claims (subject, email, issuer, temporal) are correctly extracted and returned by controllers.
  • Created a custom @WithMockJwt annotation backed by a WithSecurityContextFactory that eliminates repetitive jwt() post-processor setup across large test suites.

Ready to quiz yourself?