1. Overview

Cross-Origin Resource Sharing (CORS) is a security mechanism that allows a web page from one origin to access resources from another origin. It’s enforced by browsers to prevent websites from making unauthorized requests to different domains.

When building web applications with Spring Boot, it’s important to properly test our CORS configuration to ensure that our application can securely interact with authorized origins while blocking unauthorized ones.

More often than not, we identify CORS issues only after deploying our application. By testing our CORS configuration early, we can find and fix these problems during development itself, saving time and effort.

In this tutorial, we’ll explore how to write effective tests to verify our CORS configuration using MockMvc.

2. Configuring CORS in Spring Boot

There are various ways to configure CORS in a Spring Boot application. For this tutorial, we’ll use Spring Security and define a CorsConfigurationSource:

private CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration corsConfiguration = new CorsConfiguration();
    corsConfiguration.setAllowedOrigins(List.of("https://baeldung.com"));
    corsConfiguration.setAllowedMethods(List.of("GET"));
    corsConfiguration.setAllowedHeaders(List.of("X-Baeldung-Key"));
    corsConfiguration.setExposedHeaders(List.of("X-Rate-Limit-Remaining"));

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", corsConfiguration);
    return source;
}

In our configuration, we’re allowing requests from https://baeldung.com origin, with GET method, X-Baeldung-Key header, and exposing the X-Rate-Limit-Remaining header in the response.

We’ve hardcoded the values in our configuration, but we can use @ConfigurationProperties to externalize them.

Next, let’s configure the SecurityFilterChain bean to apply our CORS configuration:

private static final String[] WHITELISTED_API_ENDPOINTS = { "/api/v1/joke" };

@Bean
public SecurityFilterChain configure(HttpSecurity http) {
    http
      .cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource()))
      .authorizeHttpRequests(authManager -> {
        authManager.requestMatchers(WHITELISTED_API_ENDPOINTS)
          .permitAll()
          .anyRequest()
          .authenticated();
      });
    return http.build();
}

Here, we’re configuring CORS using the corsConfigurationSource() method we defined earlier.

We also whitelist the /api/v1/joke endpoint, so it can be accessed without authentication. We’ll be using this API endpoint as a base to test our CORS configuration:

private static final Faker FAKER = new Faker();

@GetMapping(value = "/api/v1/joke")
public ResponseEntity<JokeResponse> generate() {
    String joke = FAKER.joke().pun();
    String remainingLimit = FAKER.number().digit();

    return ResponseEntity.ok()
      .header("X-Rate-Limit-Remaining", remainingLimit)
      .body(new JokeResponse(joke));
}

record JokeResponse(String joke) {};

We use Datafaker to generate a random joke and a remaining rate limit value. We then return the joke in the response body and include the X-Rate-Limit-Remaining header with the generated value.

3. Testing CORS Using MockMvc

Now that we’ve configured CORS in our application, let’s write some tests to ensure it’s working as expected. We’ll use MockMvc to send requests to our API endpoint and verify the response.

3.1. Testing Allowed Origins

First, let’s test that requests from our allowed origin are successful:

mockMvc.perform(get("/api/v1/joke")
  .header("Origin", "https://baeldung.com"))
  .andExpect(status().isOk())
  .andExpect(header().string("Access-Control-Allow-Origin", "https://baeldung.com"));

We also verify that the response includes the Access-Control-Allow-Origin header for our request from the allowed origin.

Next, let’s verify that requests from non-allowed origins are blocked:

mockMvc.perform(get("/api/v1/joke")
  .header("Origin", "https://non-baeldung.com"))
  .andExpect(status().isForbidden())
  .andExpect(header().doesNotExist("Access-Control-Allow-Origin"));

3.2. Testing Allowed Methods

To test allowed methods, we’ll simulate a preflight request using the HTTP OPTIONS method:

mockMvc.perform(options("/api/v1/joke")
  .header("Origin", "https://baeldung.com")
  .header("Access-Control-Request-Method", "GET"))
  .andExpect(status().isOk())
  .andExpect(header().string("Access-Control-Allow-Methods", "GET"));

We verify that the request succeeds and the Access-Control-Allow-Methods header is present in the response.

Similarly, let’s ensure that non-allowed methods are rejected:

mockMvc.perform(options("/api/v1/joke")
  .header("Origin", "https://baeldung.com")
  .header("Access-Control-Request-Method", "POST"))
  .andExpect(status().isForbidden());

3.3. Testing Allowed Headers

Now, we’ll test allowed headers by sending a preflight request with the Access-Control-Request-Headers header and verifying the Access-Control-Allow-Headers in the response:

mockMvc.perform(options("/api/v1/joke")
  .header("Origin", "https://baeldung.com")
  .header("Access-Control-Request-Method", "GET")
  .header("Access-Control-Request-Headers", "X-Baeldung-Key"))
  .andExpect(status().isOk())
  .andExpect(header().string("Access-Control-Allow-Headers", "X-Baeldung-Key"));

And let’s verify that our application rejects non-allowed headers:

mockMvc.perform(options("/api/v1/joke")
  .header("Origin", "https://baeldung.com")
  .header("Access-Control-Request-Method", "GET")
  .header("Access-Control-Request-Headers", "X-Non-Baeldung-Key"))
  .andExpect(status().isForbidden());

3.4. Testing Exposed Headers

Finally, let’s test that our exposed header is properly included in the response for allowed origins:

mockMvc.perform(get("/api/v1/joke")
  .header("Origin", "https://baeldung.com"))
  .andExpect(status().isOk())
  .andExpect(header().string("Access-Control-Expose-Headers", "X-Rate-Limit-Remaining"))
  .andExpect(header().exists("X-Rate-Limit-Remaining"));

We verify that the Access-Control-Expose-Headers header is present in the response and includes our exposed header X-Rate-Limit-Remaining. We also check that the actual X-Rate-Limit-Remaining header exists.

Similarly, let’s ensure that our exposed header is not included in the response for non-allowed origins:

mockMvc.perform(get("/api/v1/joke")
  .header("Origin", "https://non-baeldung.com"))
  .andExpect(status().isForbidden())
  .andExpect(header().doesNotExist("Access-Control-Expose-Headers"))
  .andExpect(header().doesNotExist("X-Rate-Limit-Remaining"));

4. Conclusion

In this article, we discussed how to write effective tests using MockMvc to verify that our CORS configuration is correctly allowing requests from authorized origins, methods, and headers while blocking unauthorized ones.

By thoroughly testing our CORS configuration, we can catch misconfigurations early and prevent unexpected CORS errors in production.

As always, all the code examples used in this article are available over on GitHub.