How to unit test spring's gateway?
Asked Answered
H

3

6

My gateway will redirect traffic to many different services (under different domain names). how can i test the gateway's configuration? with only one service i can just setup the mock server (like httpbin) and test the response. with multiple services i'd prefer to avoid starting the whole docker network or changing the locak dns aliases. does spring offer any lightweight way of testing the gateway?

Harumscarum answered 20/1, 2020 at 14:51 Comment(0)
H
3

@apsisim provided a great idea to use web proxy. but the tool he suggests is not in any maven repo and has commercial license. what worked for me:

run the gateway so it will use a proxy (u can be more fancy and find a free port):

private const val proxyPort = 1080

@SpringBootTest(
    properties = [
        //"logging.level.reactor.netty.http.server=debug",
        //"spring.cloud.gateway.httpserver.wiretap=true",
        //"spring.cloud.gateway.httpclient.wiretap=true",
        "spring.cloud.gateway.httpclient.proxy.host=localhost",
        "spring.cloud.gateway.httpclient.proxy.port=$proxyPort"
    ]
)

then use the mockwebserver as a proxy

testImplementation("com.squareup.okhttp3:mockwebserver:4.2.1")
testImplementation("com.squareup.okhttp3:okhttp:4.2.1")

and then all your requests will go to your proxy. just remember that http protocol specifies that first request to new server requires tunneling via the proxy so when u do first request to the gateway, the gateway will send 2 requests to the proxy:

testClient.get()
            .uri(path)
            .header("host", gatewayDns)
            .exchange()

nextRequestFromGateway {
    method `should equal` "CONNECT"
    headers[HOST] `should equal` "$realUrlBehindGateway:80"
}

nextRequestFromGateway {
    path `should equal` "/api/v1/whatever"
    headers[HOST] `should equal` realUrlBehindGateway
}

...
fun nextRequestFromGateway(block : RecordedRequest.() -> Unit) {
    mockWebServer.takeRequest().apply (block)
}
Harumscarum answered 27/1, 2020 at 23:48 Comment(1)
hey, is there full code or repository for this test?Maryrosemarys
T
5

Here is how to achieve what you want with the API Simulator:

package my.package;

import static com.apisimulator.embedded.SuchThat.isEqualTo;
import static com.apisimulator.embedded.SuchThat.startsWith;
import static com.apisimulator.embedded.http.HttpApiSimulation.httpApiSimulation;
import static com.apisimulator.embedded.http.HttpApiSimulation.httpRequest;
import static com.apisimulator.embedded.http.HttpApiSimulation.httpResponse;
import static com.apisimulator.embedded.http.HttpApiSimulation.simlet;
import static com.apisimulator.http.Http1Header.CONTENT_TYPE;
import static com.apisimulator.http.HttpMethod.CONNECT;
import static com.apisimulator.http.HttpMethod.GET;
import static com.apisimulator.http.HttpStatus.OK;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;

import java.time.Duration;
import java.util.Map;

import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.SocketUtils;

import com.apisimulator.embedded.http.JUnitHttpApiSimulation;

@RunWith(SpringRunner.class)
@SpringBootTest(
   webEnvironment = RANDOM_PORT,
   properties = { 
      "management.server.port=${test.port}", "logging.level.root=info",
      // Configure the Gateway to use HTTP proxy - the API Simulator 
      // instance running at localhost:6090
      "spring.cloud.gateway.httpclient.proxy.host=localhost",
      "spring.cloud.gateway.httpclient.proxy.port=6090"
      //"logging.level.reactor.netty.http.server=debug",
      //"spring.cloud.gateway.httpserver.wiretap=true" 
   }
)
@Import(ServiceGatewayApplication.class)
public class ServiceGatewayApplicationTest
{

   // Configure an API simulation. This starts up an instance 
   // of API Simulator on localhost, default port 6090
   @ClassRule
   public static final JUnitHttpApiSimulation clApiSimulation = JUnitHttpApiSimulation
            .as(httpApiSimulation("svc-gateway-backends"));

   protected static int managementPort;

   @LocalServerPort
   protected int port = 0;

   protected String baseUri;
   protected WebTestClient webClient;

   @BeforeClass
   public static void beforeClass()
   {
      managementPort = SocketUtils.findAvailableTcpPort();
      System.setProperty("test.port", String.valueOf(managementPort));

      // Configure simlets for the API simulation
      // @formatter:off
      clApiSimulation.add(simlet("http-proxy")
         .when(httpRequest(CONNECT))
         .then(httpResponse(OK))
      );

      clApiSimulation.add(simlet("test-domain-1")
         .when(httpRequest()
               .whereMethod(GET)
               .whereUriPath(isEqualTo("/static"))
               // The `host` header is used to determine the actual destination 
               .whereHeader("host", startsWith("domain-1.com"))
          )
         .then(httpResponse()
               .withStatus(OK)
               .withHeader(CONTENT_TYPE, "application/text")
               .withBody("{ \"domain\": \"1\" }")
          )
      );

      clApiSimulation.add(simlet("test-domain-2")
         .when(httpRequest()
               .whereMethod(GET)
               .whereUriPath(isEqualTo("/v1/api/foo"))
               .whereHeader("host", startsWith("domain-2.com"))
          )
         .then(httpResponse()
               .withStatus(OK)
               .withHeader(CONTENT_TYPE, "application/json; charset=UTF-8")
               .withBody(
                  "{\n" +
                  "   \"domain\": \"2\"\n" + 
                  "}"
                )
          )
      );
      // @formatter:on
   }

   @AfterClass
   public static void afterClass()
   {
      System.clearProperty("test.port");
   }

   @Before
   public void setup()
   {
      // @formatter:off
      baseUri = "http://localhost:" + port;
      webClient = WebTestClient.bindToServer()
         .baseUrl(baseUri)
         .responseTimeout(Duration.ofSeconds(2))
         .build();
      // @formatter:on
   }

   @Test
   public void test_domain1()
   {
      // @formatter:off
      webClient.get()
         .uri("/static")
         .exchange()
         .expectStatus().isOk()
         .expectBody(String.class).consumeWith(result -> 
             assertThat(result.getResponseBody()).isEqualTo("{ \"domain\": \"1\" }")
          );
      // @formatter:on
   }

   @Test
   public void test_domain2()
   {
      // @formatter:off
      webClient.get()
         .uri("/v1/api/foo")
         .exchange()
         .expectStatus().isOk()
         .expectHeader()
            .contentType("application/json; charset=UTF-8")
         .expectBody(Map.class).consumeWith(result -> 
             assertThat(result.getResponseBody()).containsEntry("domain", "2")
          );
      // @formatter:on
   }

}

Most of the code is based on this GatewaySampleApplicationTests class from the Spring Cloud Gateway project.

The above assumes the Gateway has routes similar to these (snippets only):

    ...
    uri: "http://domain-1.com"
    predicates:
      - Path=/static
    ...
    uri: "http://domain-2.com"
    predicates:
      - Path=/v1/api/foo
    ...
Trix answered 20/1, 2020 at 19:49 Comment(0)
H
3

@apsisim provided a great idea to use web proxy. but the tool he suggests is not in any maven repo and has commercial license. what worked for me:

run the gateway so it will use a proxy (u can be more fancy and find a free port):

private const val proxyPort = 1080

@SpringBootTest(
    properties = [
        //"logging.level.reactor.netty.http.server=debug",
        //"spring.cloud.gateway.httpserver.wiretap=true",
        //"spring.cloud.gateway.httpclient.wiretap=true",
        "spring.cloud.gateway.httpclient.proxy.host=localhost",
        "spring.cloud.gateway.httpclient.proxy.port=$proxyPort"
    ]
)

then use the mockwebserver as a proxy

testImplementation("com.squareup.okhttp3:mockwebserver:4.2.1")
testImplementation("com.squareup.okhttp3:okhttp:4.2.1")

and then all your requests will go to your proxy. just remember that http protocol specifies that first request to new server requires tunneling via the proxy so when u do first request to the gateway, the gateway will send 2 requests to the proxy:

testClient.get()
            .uri(path)
            .header("host", gatewayDns)
            .exchange()

nextRequestFromGateway {
    method `should equal` "CONNECT"
    headers[HOST] `should equal` "$realUrlBehindGateway:80"
}

nextRequestFromGateway {
    path `should equal` "/api/v1/whatever"
    headers[HOST] `should equal` realUrlBehindGateway
}

...
fun nextRequestFromGateway(block : RecordedRequest.() -> Unit) {
    mockWebServer.takeRequest().apply (block)
}
Harumscarum answered 27/1, 2020 at 23:48 Comment(1)
hey, is there full code or repository for this test?Maryrosemarys
A
0

I thought I would provide my solution as well, since it has the benefit of not including any third party tools/dependencies:

Provided I have following configuration I want to test:

spring:
  cloud:
    gateway:
      routes:
        - id: testRoute
          uri: http://localhost:8999
          predicates:
            - Path=/test
          filters:
            - RewritePath=/test, /foo.json

and that I have an empty foo.json file under /src/test/resources/static folder

the following test will work:

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.server.LocalServerPort;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.test.web.reactive.server.WebTestClient;

import java.net.URI;
import java.util.List;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class TestApiGatewayApplicationTests {

    @LocalServerPort int localPort;

    @Autowired GatewayProperties gatewayProperties;
    @Autowired ApplicationEventPublisher eventPublisher;
    @Autowired private WebTestClient webTestClient;

    @Test
    void testWhitelistedUrl() {
        // GIVEN I updated the port
        List<RouteDefinition> routes = gatewayProperties.getRoutes();
        routes.get(0).setUri(
            URI.create("http://localhost:%s".formatted(localPort))
        );
        eventPublisher.publishEvent(new RefreshRoutesEvent(this));
 
        // WHEN I call test THEN the config works
        webTestClient.get().uri("/test").exchange().expectStatus().isOk();
    }
}

Since the main issue is that the route doesn't have the correct port, I simply define the port in the test itself and then use RefreshRoutesEvent to trigger routes refresh.

I opted for this approach with WebEnvironment.RANDOM_PORT specifically because I needed my tests to also work when running on parallel on Jenkins server.

Andrewandrewes answered 19/7, 2024 at 21:25 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.