Creating a Complete CRUD API with Spring Boot 3: Authentication, Authorization, and Testing




In this article, I will explore how to create a complete application that implements a CRUD API using Spring Boot 3. I will perform Create, Read, Update, and Delete operations on a database, implement authentication and authorization with JSON Web Token (JWT), and develop a REST API for managing orders in an e-commerce system. Finally, I will test the API using JUnit.

Table of Contents

  • Technologies Used
  • Client-Server Interactions for Order Retrieval
  • Creating a CRUD API
    • Project Setup
    • Structuring the Project
    • Database Configuration
    • Creating Entity and Repository
    • Implementing Service and Controller
  • Authentication and Authorization with JWT
    • Configuring Spring Security
    • Creating JWT Tokens
    • Implementing the Login Endpoint
  • Swagger setup
  • Testing APIs with JUnit
    • Testing the Service
    • Testing the Controller
    • Running Tests with Maven
  • Testing Authentication
  • Testing Order Retrieval with Authentication
  • Advanced Topics and Best Practices
  • Conclusion
  • References

Technologies Used

  • Spring Boot: Framework for rapid configuration and application development.
  • Spring Data JPA: Module for interacting with relational databases.
  • H2 Database: Lightweight in-memory database for testing and development.
  • Swagger: Tool for documenting and testing REST APIs.
  • Spring Security: Framework for managing authentication and authorization.
  • JWT (JSON Web Token): Standard for stateless authentication.
  • JUnit: Library for unit testing in Java.

Client-Server Interactions for Order Retrieval

When a client requests order data, the sequence is as follows:

  1. Client Authentication:
    • The client sends a login request with credentials to the /api/auth/login endpoint.
    • The server verifies the credentials and generates a JWT token.
  2. JWT Token Storage:
    • The client stores the JWT token for use in subsequent requests.
  3. Protected Request to the Server:
    • The client sends a GET request to the /api/orders endpoint, including the JWT token in the Authorization header.
    • The server verifies the token and processes the request.
  4. Response to the Client:
    • The server returns the order data in JSON format with HTTP status 200 (OK).

Client-Server Interactions for Order Retrieval


Creating a CRUD API

Project Setup

To create the project, use Spring Initializr and include the following dependencies:

  • Spring Web: Tools for developing web applications, including RESTful endpoints.
  • Spring Data JPA: Enables working with relational databases using ORM.
  • H2 Database: Lightweight in-memory database for rapid testing and development.
  • Spring Security: Provides advanced authentication and authorization features.
  • Spring Boot DevTools: Enhances productivity with automatic restarts during development.
  • Lombok: Reduces boilerplate code for getters, setters, and constructors.
pom.xml file should look like the following:

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.6.0</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId> 
    <version>0.11.5</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Structuring the Project

Structuring a Spring Boot project well ensures maintainability and scalability. Here’s a suggested project structure:

src/
├── main/
│   ├── java/
│   │   └── com.example.project/
│   │       ├── config/
│   │       ├── controller/
│   │       ├── dto/
│   │       ├── entity/
│   │       ├── exception/
│   │       ├── repository/
│   │       ├── security/
│   │       ├── service/
│   │       └── ProjectApplication.java
│   └── resources/
│       ├── application.properties
│       ├── application-dev.properties
│       └── application-prod.properties
└── test/

Key Folders and Their Roles:

  1. config/: Configuration files for security, CORS, and other application-wide settings.
  2. controller/: REST endpoints.
  3. dto/: Data Transfer Objects for requests and responses.
  4. entity/: Database entities.
  5. exception/: Custom exception handling.
  6. repository/: Interfaces for interacting with the database.
  7. service/: Business logic and application rules.
  8. security/: Classes for JWT management and authentication filters.

This structure ensures the code is modular and easy to navigate, even in larger projects.

Database Configuration

In this section, I describe how to configure different database profiles to support development and production environments. The profile-based approach allows you to quickly switch configurations without altering the code.

  • Development Profile: Uses an in-memory H2 database for quick testing and simple configurations.
  • Production Profile: Uses a PostgreSQL database configured for data persistence in real-world environments.

Development Profile Configuration (application-dev.properties)

spring.datasource.url=jdbc:h2:mem:testdb

spring.datasource.driver-class-name=org.h2.Driver

spring.datasource.username=sa

spring.datasource.password=password

spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

spring.h2.console.enabled=true

spring.jpa.hibernate.ddl-auto=none

springdoc.api-docs.enabled=true

springdoc.swagger-ui.enabled=true

spring.main.allow-bean-definition-overriding=true

This configuration enables the H2 console for easy data visualization during debugging and development.

Production Profile Configuration (application-prod.properties)

spring.datasource.url=jdbc:postgresql://localhost:5432/mydb

spring.datasource.username=myuser

spring.datasource.password=mypassword

spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect

spring.jpa.hibernate.ddl-auto=none

springdoc.api-docs.enabled=true

springdoc.swagger-ui.enabled=true

spring.main.allow-bean-definition-overriding=true

This configuration ensures that PostgreSQL is used for data management in production, with secure credentials.

Selecting the Active Profile

To run the application with a specific profile, use the -Dspring.profiles.active parameter when executing the command:

  • Development: bash java -Dspring.profiles.active=dev -jar target/myapp.jar
  • Production: bash java -Dspring.profiles.active=prod -jar target/myapp.jar

Creating the Entity and Repository

An entity in Spring represents a database table and is mapped using JPA annotations. Here, I create a class Order to represent an order in the system and provide a repository to perform persistence operations.

Order Class

  • @Entity: Specifies that this class is a JPA entity.
  • @Id and @GeneratedValue: Identify the primary key and automate its generation.
  • Fields: Contain order details such as product name, quantity, price, and status.
import org.springframework.data.annotation.Id;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.ManyToOne;

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String productName;
    private int quantity;
    private double price;
    private String status;

    @ManyToOne
    private Customer customer;

    // Getters, Setters, and Constructors
}

You can also define relationships with other entities such as Customer, using annotations like @ManyToOne or @OneToMany for more complex data models.

Customer Entity

The Customer entity represents the customers placing orders. It has a one-to-many relationship with the Order entity. 
@Entity
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // Genera automaticamente un ID univoco
    private Long id;

    private String name;

    private String email;

    @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Order> orders = new ArrayList();

    // Getters, Setters, and Constructors
}

Repository

The repository extends JpaRepository to provide predefined CRUD methods. It also defines custom queries to filter orders by status or search by product name.

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.example.demo.entity.Order;

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    List<Order> findByStatus(String status);
    List<Order> findByProductNameContaining(String productName);
}

This configuration ensures seamless interaction with the database while minimizing the code required for common persistence operations.

Service and Controller Implementation

Service

The service (OrderService) is the business logic layer of the application, responsible for interacting with the repository and performing operations on order data. It provides methods to create, read, update, and delete orders, ensuring a separation of business logic from REST endpoints.

  • Constructor Dependency Injection: Uses constructor-based dependency injection to keep the code modular and testable.
  • Error Handling: Implements custom exceptions to handle error cases, such as entities not found.
import java.util.List;

import org.springframework.stereotype.Service;

import com.example.demo.entity.Order;
import com.example.demo.repository.OrderRepository;

import jakarta.persistence.EntityNotFoundException;

@Service
public class OrderService {
	private final OrderRepository orderRepository;

	public OrderService(OrderRepository orderRepository) {
		this.orderRepository = orderRepository;
	}

	public Order saveOrder(Order order) {
		return orderRepository.save(order);
	}

	public List<Order> getAllOrders() {
		return orderRepository.findAll();
	}

	public Order updateOrder(Long id, Order order) {
		return orderRepository.findById(id).map(existingOrder -> {
			existingOrder.setProductName(order.getProductName());
			existingOrder.setQuantity(order.getQuantity());
			existingOrder.setPrice(order.getPrice());
			existingOrder.setStatus(order.getStatus());
			return orderRepository.save(existingOrder);
		}).orElseThrow(() -> new EntityNotFoundException("Order not found"));
	}

	public void deleteOrder(Long id) {
		orderRepository.deleteById(id);
	}
}

Controller

The controller (OrderController) acts as the entry point for HTTP requests. It translates REST calls into service operations and returns responses to the client in JSON format.

  • Swagger Annotations: Provides interactive and readable documentation for each API endpoint.
  • Endpoint Mapping: Each method in the controller is associated with a specific HTTP operation (POST, GET, PUT, DELETE) and a well-defined endpoint, ensuring clear API structure.
import java.util.List;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
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.entity.Order;
import com.example.demo.service.OrderService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;

@RestController
@RequestMapping("/api/orders")
@Tag(name = "Order API", description = "API for managing orders")
public class OrderController {
	private final OrderService orderService;

	public OrderController(OrderService orderService) {
		this.orderService = orderService;
	}

	@PostMapping
	@Operation(summary = "Create a new order", description = "Add a new order to the system", security = @SecurityRequirement(name = "bearerAuth"))
	@ApiResponse(responseCode = "201", description = "Order created successfully")
	public ResponseEntity<Order> createOrder(@RequestBody Order order) {
		return new ResponseEntity<>(orderService.saveOrder(order), HttpStatus.CREATED);
	}

	@GetMapping
    @Operation(summary = "Get all orders", description = "Retrieve all orders", security = @SecurityRequirement(name = "bearerAuth"))
	@ApiResponse(responseCode = "200", description = "List of orders retrieved successfully")
	public List<Order> getAllOrders() {
		return orderService.getAllOrders();
	}

	@PutMapping("/{id}")
	@Operation(summary = "Update an order", description = "Update an existing order by its ID", security = @SecurityRequirement(name = "bearerAuth"))
	@ApiResponse(responseCode = "200", description = "Order updated successfully")
	public Order updateOrder(@PathVariable Long id, @RequestBody Order order) {
		return orderService.updateOrder(id, order);
	}

	@DeleteMapping("/{id}")
	@Operation(summary = "Delete an order", description = "Remove an order from the system by its ID", security = @SecurityRequirement(name = "bearerAuth"))
	@ApiResponse(responseCode = "204", description = "Order deleted successfully")
	public ResponseEntity<Void> deleteOrder(@PathVariable Long id) {
		orderService.deleteOrder(id);
		return ResponseEntity.noContent().build();
	}
}

This implementation ensures a clean separation of concerns, making the codebase maintainable and scalable.

Authentication and Authorization with JWT

Authentication and authorization are implemented using JSON Web Token (JWT). This approach enables a stateless system, ideal for distributed and scalable applications.

Configuring Spring Security

Spring Security is configured to protect API endpoints. Authentication endpoints are public, while all other endpoints require a valid JWT token.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.example.demo.filter.JwtAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	private JwtAuthenticationFilter jwtAuthenticationFilter;
	
	public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
		this.jwtAuthenticationFilter=jwtAuthenticationFilter;
	}
	
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
    
    @Bean
    public InMemoryUserDetailsManager userDetailsManager() {
        return new InMemoryUserDetailsManager(
            User.withUsername("user")
                .password(passwordEncoder().encode("password"))
                .roles("USER")
                .build(),
            User.withUsername("admin")
                .password(passwordEncoder().encode("admin"))
                .roles("ADMIN")
                .build()
        );
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    	http
        .csrf(csrf -> csrf.disable())
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/api/auth/**").permitAll() // Consenti endpoint pubblici
            .requestMatchers("/api/orders/**").hasAuthority("ROLE_ADMIN") // Solo ADMIN può accedere
            .anyRequest().authenticated() // Tutti gli altri endpoint richiedono autenticazione
        )
    	.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); // Aggiungi il filtro JWT
        return http.build();
    }
}

JWT Token Generation and Validation

The JWT token is generated using a secret key and contains user information.

Example class for JWT token management:

import java.util.Date;
import java.util.List;

import org.springframework.stereotype.Component;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

@Component
public class JwtTokenUtil {

    public String generateToken(String username, List<String> roles) {
        return Jwts.builder()
                .setSubject(username)
                .claim("roles", roles) 
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + 86400000)) // 1 giorno
                .signWith(JwtKeyUtil.getKey(), SignatureAlgorithm.HS256)
                .compact();
    }
}

Example class for JWT token generator:

import java.security.Key;

import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

public class JwtKeyUtil {
    private final static Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);

    public static Key getKey() {
        return key;
    }
}

Login Endpoint

The controller handles JWT token generation during login.

import java.util.List;
import java.util.stream.Collectors;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
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.dto.AuthRequest;
import com.example.demo.dto.AuthResponse;
import com.example.demo.security.JwtTokenUtil;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JwtTokenUtil jwtTokenUtil;
    private final UserDetailsService userDetailsService;
    
    public AuthController(AuthenticationManager authenticationManager, JwtTokenUtil jwtTokenUtil, UserDetailsService userDetailsService) {
        this.authenticationManager = authenticationManager;
        this.jwtTokenUtil = jwtTokenUtil;
		this.userDetailsService = userDetailsService;
    }

    @PostMapping("/login")
    public ResponseEntity<AuthResponse> login(@RequestBody AuthRequest authRequest) {
        try {
            authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword()));

            UserDetails userDetails = userDetailsService.loadUserByUsername(authRequest.getUsername());
            List<String> roles = userDetails.getAuthorities()
                                            .stream()
                                            .map(GrantedAuthority::getAuthority)
                                            .collect(Collectors.toList());

            
            String token = jwtTokenUtil.generateToken(authRequest.getUsername(), roles);
            return ResponseEntity.ok(new AuthResponse(token));
        } catch (BadCredentialsException e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null);
        }
    }
}

DTO (Data Transfer Object)

Login Request AuthRequest Represents the payload for login requests. :

public class AuthRequest {
    private String username;
    private String password;
    // Getters and Setters
}

Login Response AuthResponse Represents the payload for login responses. :

public class AuthResponse {
    private String token;

public AuthResponse(String token) {
    this.token = token;
}

public String getToken() {
    return token;
}
}

Configurazione del Filtro JWT

The JwtAuthenticationFilter is essential to validate the JWT token and set the security context correctly in the Spring Boot application. Below is a complete implementation example. 
JwtAuthenticationFilter Implementation 
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import com.example.demo.security.JwtKeyUtil;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, jakarta.servlet.ServletException {

        // Leggi l'header Authorization
        String header = request.getHeader("Authorization");

        if (header == null || !header.startsWith("Bearer ")) {
            chain.doFilter(request, response);
            return; // Non è presente un token, procedi senza autenticazione
        }

        String token = header.substring(7); // Rimuove "Bearer " dall'inizio
        try {
            // Decodifica il token JWT
            Claims claims = Jwts.parserBuilder()
                    .setSigningKey(JwtKeyUtil.getKey())
                    .build()
                    .parseClaimsJws(token)
                    .getBody();

            String username = claims.getSubject(); // Ottieni lo username
            List<String> roles = claims.get("roles", List.class); // Ottieni i ruoli (es. "ADMIN")

            List<SimpleGrantedAuthority> authorities = roles.stream()
                    .map(role -> new SimpleGrantedAuthority(role)) // Aggiungi il prefisso "ROLE_"
                    .collect(Collectors.toList());

            // Crea un oggetto Authentication
            UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(
                            username,
                            null,
                            authorities // Passa la lista di authorities
                    );
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            // Imposta il contesto di sicurezza
            SecurityContextHolder.getContext().setAuthentication(authentication);

        } catch (Exception e) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // Token non valido
            return;
        }

        chain.doFilter(request, response);
    }
}

Swagger setup

The Swagger setup is configured using a Spring configuration class.

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SwaggerConfig {

    @Bean
    public OpenAPI customOpenAPI() {
        // Definizione dello schema di sicurezza
        SecurityScheme securityScheme = new SecurityScheme()
            .type(SecurityScheme.Type.HTTP)
            .scheme("bearer")
            .bearerFormat("JWT");

        // Aggiunge lo schema di sicurezza e lo applica globalmente
        return new OpenAPI()
            .components(new Components().addSecuritySchemes("bearerAuth", securityScheme))
            .addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
            .info(new Info()
                .title("API Documentation")
                .version("1.0.0")
                .description("Documentazione delle API con Swagger e JWT"));
    }
}

API Testing with JUnit

Service Testing

Service layer tests verify business logic without dependencies on the controller layer. For example, the following test ensures that an order is saved correctly to the database:

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.example.demo.entity.Order;
import com.example.demo.repository.OrderRepository;
import com.example.demo.service.OrderService;

import jakarta.persistence.EntityNotFoundException;

@ExtendWith(MockitoExtension.class)
public class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @InjectMocks
    private OrderService orderService;

    @Test
    public void testSaveOrder() {
        // Arrange
        Order order = new Order();
        order.setProductName("Laptop");
        order.setQuantity(1);
        order.setPrice(1200.00);

        when(orderRepository.save(order)).thenReturn(order);

        // Act
        Order savedOrder = orderService.saveOrder(order);

        // Assert
        assertNotNull(savedOrder);
        assertEquals("Laptop", savedOrder.getProductName());
        assertEquals(1, savedOrder.getQuantity());
        assertEquals(1200.00, savedOrder.getPrice());
        verify(orderRepository, times(1)).save(order);
    }

    @Test
    public void testGetAllOrders() {
        // Arrange
        Order order1 = new Order();
        order1.setProductName("Laptop");

        Order order2 = new Order();
        order2.setProductName("Phone");

        when(orderRepository.findAll()).thenReturn(Arrays.asList(order1, order2));

        // Act
        List<Order> orders = orderService.getAllOrders();

        // Assert
        assertNotNull(orders);
        assertEquals(2, orders.size());
        assertEquals("Laptop", orders.get(0).getProductName());
        assertEquals("Phone", orders.get(1).getProductName());
        verify(orderRepository, times(1)).findAll();
    }

    @Test
    public void testUpdateOrder_Success() {
        // Arrange
        Long orderId = 1L;
        Order existingOrder = new Order();
        existingOrder.setId(orderId);
        existingOrder.setProductName("Laptop");
        existingOrder.setQuantity(1);

        Order updatedOrder = new Order();
        updatedOrder.setProductName("Updated Laptop");
        updatedOrder.setQuantity(2);

        when(orderRepository.findById(orderId)).thenReturn(Optional.of(existingOrder));
        when(orderRepository.save(existingOrder)).thenReturn(existingOrder);

        // Act
        Order result = orderService.updateOrder(orderId, updatedOrder);

        // Assert
        assertNotNull(result);
        assertEquals("Updated Laptop", result.getProductName());
        assertEquals(2, result.getQuantity());
        verify(orderRepository, times(1)).findById(orderId);
        verify(orderRepository, times(1)).save(existingOrder);
    }

    @Test
    public void testUpdateOrder_NotFound() {
        // Arrange
        Long orderId = 1L;
        Order updatedOrder = new Order();
        updatedOrder.setProductName("Updated Laptop");

        when(orderRepository.findById(orderId)).thenReturn(Optional.empty());

        assertThrows(EntityNotFoundException.class, () -> orderService.updateOrder(orderId, updatedOrder));
        verify(orderRepository, times(1)).findById(orderId);
        verify(orderRepository, never()).save(any(Order.class));
    }

    @Test
    public void testDeleteOrder() {
        // Arrange
        Long orderId = 1L;

        // Act
        orderService.deleteOrder(orderId);

        // Assert
        verify(orderRepository, times(1)).deleteById(orderId);
    }
}

Controller Testing

Before testing REST endpoints, you need to obtain a valid JWT token. This is done by simulating a request to the authentication service. The token is then used to authenticate subsequent requests to the controller.

Example controller test:

@WebMvcTest(OrderController.class)
public class OrderControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private OrderService orderService;

@MockBean
private JwtTokenUtil jwtTokenUtil;

@MockBean
private AuthenticationManager authenticationManager;

private String obtainToken() throws Exception {
    AuthRequest authRequest = new AuthRequest("testUser", "testPassword");
    String token = "mockedToken";

    when(jwtTokenUtil.generateToken(anyString())).thenReturn(token);

    mockMvc.perform(post("/api/auth/login")
            .contentType(MediaType.APPLICATION_JSON)
            .content(new ObjectMapper().writeValueAsString(authRequest)))
            .andExpect(status().isOk());

    return token;
}

@Test
public void testCreateOrderWithToken() throws Exception {
    String token = obtainToken();

    Order order = new Order("Laptop", 2, 1500.0, "Pending");

    when(orderService.saveOrder(any(Order.class))).thenReturn(order);

    mockMvc.perform(post("/api/orders")
            .header("Authorization", "Bearer " + token)
            .contentType(MediaType.APPLICATION_JSON)
            .content(new ObjectMapper().writeValueAsString(order)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.productName").value("Laptop"));
}
}

Running Tests with Maven

To execute tests with Maven, use the following command specifying the desired profile with the -Dspring.profiles.active parameter:

  • Run tests with the development profile (H2 Database): bash mvn test -Dspring.profiles.active=dev

  • Run tests with the production profile (PostgreSQL): bash mvn test -Dspring.profiles.active=prod

This command ensures tests are executed using configurations specific to the active profile. Results will be displayed in the console, including details about any errors or failures. With Maven, you can also generate detailed test reports using plugins like Surefire for in-depth analysis.

Testing Authentication

Testing AuthController

@ExtendWith(MockitoExtension.class)
public class AuthControllerTest {

    @Mock
    private JwtTokenUtil jwtTokenUtil;

    @Mock
    private AuthenticationManager authenticationManager;

    @InjectMocks
    private AuthController authController;

    @Test
    public void testLogin_Success() {
        AuthRequest authRequest = new AuthRequest();
        authRequest.setUsername("testUser");
        authRequest.setPassword("testPassword");

        String mockToken = "mockJwtToken";

        when(jwtTokenUtil.generateToken("testUser")).thenReturn(mockToken);

        ResponseEntity<AuthResponse> response = authController.login(authRequest);

        assertNotNull(response);
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertNotNull(response.getBody());
        assertEquals(mockToken, response.getBody().getToken());
    }

    @Test
    public void testLogin_InvalidCredentials() {
        AuthRequest authRequest = new AuthRequest();
        authRequest.setUsername("testUser");
        authRequest.setPassword("wrongPassword");

        doThrow(new BadCredentialsException("Invalid credentials"))
            .when(authenticationManager)
            .authenticate(any());

        ResponseEntity<AuthResponse> response = authController.login(authRequest);

        assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
    }
}
Testing JwtTokenUtil
public class JwtTokenUtilTest {

    private JwtTokenUtil jwtTokenUtil;

    @BeforeEach
    public void setUp() {
        jwtTokenUtil = new JwtTokenUtil();
    }

    @Test
    public void testGenerateToken() {
        String token = jwtTokenUtil.generateToken("testUser");
        assertNotNull(token);
    }

    @Test
    public void testValidateToken() {
        String token = jwtTokenUtil.generateToken("testUser");
        assertTrue(jwtTokenUtil.validateToken(token));
    }

    @Test
    public void testExtractUsername() {
        String token = jwtTokenUtil.generateToken("testUser");
        assertEquals("testUser", jwtTokenUtil.extractUsername(token));
    }
}

Testing Authentication and Protected Endpoints

The following JUnit test retrieves a JWT token and uses it to access the protected /api/orders endpoint:
Integration Test for Order Retrieval

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;

import com.example.demo.DemoApplication;
import com.example.demo.dto.AuthRequest;
import com.example.demo.dto.AuthResponse;
import com.fasterxml.jackson.databind.ObjectMapper;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = DemoApplication.class)
public class OrderControllerIntegrationTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    public void testGetOrdersWithJwtToken() throws Exception {
        // Step 1: Retrieve a JWT token
        String authUrl = "http://localhost:" + port + "/api/auth/login";
        AuthRequest authRequest = new AuthRequest();
        authRequest.setUsername("admin");
        authRequest.setPassword("admin");
        
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        HttpEntity<AuthRequest> authEntity = new HttpEntity<>(authRequest, headers);

        ResponseEntity<String> authResponse = restTemplate.exchange(
                authUrl,
                HttpMethod.POST,
                authEntity,
                String.class
        );

        assertEquals(HttpStatus.OK, authResponse.getStatusCode());

        AuthResponse authResponseBody = objectMapper.readValue(authResponse.getBody(), AuthResponse.class);
        String token = authResponseBody.getToken();
        assertNotNull(token);

        // Step 2: Use the token to call the protected endpoint
        String ordersUrl = "http://localhost:" + port + "/api/orders";

        HttpHeaders orderHeaders = new HttpHeaders();
        orderHeaders.setContentType(MediaType.APPLICATION_JSON);
        orderHeaders.set("Authorization", "Bearer " + token);

        HttpEntity<Void> orderEntity = new HttpEntity<>(null, orderHeaders);

        ResponseEntity<String> orderResponse = restTemplate.exchange(
                ordersUrl,
                HttpMethod.GET,
                orderEntity,
                String.class
        );

        assertEquals(HttpStatus.OK, orderResponse.getStatusCode());
        assertNotNull(orderResponse.getBody());
    }
}

Advanced Topics and Best Practices

Token Expiry and Refresh

  • Tokens should expire after a set time (e.g., 15 minutes).
  • Implement a refresh token mechanism to issue new tokens securely.

Securing Secrets

  • Store the secret key securely (e.g., in an environment variable or secret management tool).

Performance Optimizations

  • Use caching (e.g., Redis) to reduce database queries for frequently accessed data.
  • Optimize database queries with indexes.

Monitoring and Logging

  • Use tools like Prometheus and Grafana to monitor API performance.
  • Implement centralized logging using ELK Stack.

User data validation

  • Implement data validation on DTO classes

Conclusion

I created a complete application using Spring Boot to implement a CRUD API, secured it with JWT, and tested it with JUnit. This approach offers numerous advantages, including:

  • Modularity: Separation of service, controller, and repository layers ensures clean and maintainable design.
  • Security: Using JWT allows for stateless authentication, ideal for scalable applications.
  • Testability: JUnit and MockMvc make automated testing straightforward, improving code quality.

Next Steps

For further exploration, you might consider: 1. Extending Functionality: Adding new entities or endpoints to cover more use cases.
2. Deployment: Configuring the application for deployment in a cloud environment like AWS or Azure.
3. Performance Optimization: Implementing caching or optimizing database queries for greater efficiency.

By following these steps, you can build robust, production-ready applications and extend the project for more complex use cases.

References

  1. Spring Boot Documentation
  2. JWT Documentation
  3. Spring Security Guide
  4. JUnit Documentation
Are you ready to build secure and robust APIs with Spring Boot, JWT? Dive into the concepts, implement the examples, and take your skills to the next level. Don’t forget to test your API using JUnit and share your success stories in the comments below! 🚀

About Me

I am passionate about IT technologies. If you’re interested in learning more or staying updated with my latest articles, feel free to connect with me on:

Feel free to reach out through any of these platforms if you have any questions!

I am excited to hear your thoughts! 👇

Comments

Popular posts from this blog

Monitoring and Logging with Prometheus: A Practical Guide

Logging in Spring Boot 3: Best Practices for Configuration and Implementation