background

As microservices practice: As described in the BFF case of Spring Cloud Gateway + AWS Cognito, our microservices group uses Spring Cloud Gateway as the API authentication Gateway. Spring Security provides OAuth authentication capabilities for API authentication gateways and back-end microservices.

What do we want to do

  • Want to test a single microservice
  • Wanted to test the OAuth certification process

What don’t we want to do

  • You don’t want to deploy all microservices for testing
  • Do not want to connect to a real OAuth authentication server during the test

Related test types

When we try to test microservices that communicate with other services, we can do one of two things:

  1. Deploy all microservices and perform end-to-end testing
  2. Simulate other microservices in unit/integration tests

Both have their advantages and disadvantages. First, for end-to-end testing:

Advantages:

  • Simulated production environment
  • Test real communication between services

Disadvantages:

  • To test a microservice, we must deploy multiple microservices, multiple databases, and so on
  • The environment where the tests are executed is locked and occupied exclusively by one set of tests (that is, no one else can run the tests during that time)
  • Test execution takes a long time
  • Feedback comes late
  • Extremely difficult to debug

Second, for unit/integration testing:

Advantages:

  • Very fast feedback
  • No infrastructure requirements

Disadvantages:

  • The developers of the microservice being tested are responsible for stubbing themselves, so the test stubs may differ from the real implementation
  • Even if the test passes, the production environment can still fail

You can see that these two tests actually complement each other. Both can be arranged as the project schedule and resources allow. But when people are scarce and time limit is tight, I personally prefer the latter. The reason is that we can implement testing quickly and get feedback quickly, reducing the time limit; In addition, some technical measures can be taken to avoid its disadvantages. For example, API specifications based on OAS (Open API Specification) are developed first, and API Providers and consumers are developed based on the same OAS. This reduces the difference between the test pile and the actual implementation.

The discussion in this article will focus on the latter.

API gateway testing

In the Spring Security documentation, WebTestClient is recommended for testing Webflux based responsive applications. It makes it easy to set up the authentication information we need for testing. In the following test on the class using the @ AutoConfigureWebTestClient for automatic configuration.

In addition, com. Making. Tomakehurst. Wiremock. Client. Wiremock can help us to quickly set up test pile (stubs), used in the following test class @ AutoConfigureWireMock (port = 0) automatic configuration, Port = 0 is set to a random port. The port value can be obtained from the ${wiremock.server.port} parameter.

In addition, we specified @ActiveProfiles(“test”), so the configuration in application-test.yaml takes effect when executing this test class.

@AutoConfigureWebTestClient
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWireMock(port = 0)
@ActiveProfiles("test")
class ApiGatewayApplicationTests {

    @Autowired
    private WebTestClient client;
 
    // ...
}
Copy the code

To use Stub URIs for testing, we need to modify the Controller by first configuring the URI to application.yaml and then obtaining it via @value (“${order-service-uri}”).

@RestController
public class CompositionController {

    @Value("${order-service-uri}")
    private String orderServiceUri;

    @Value("${storage-service-uri}")
    private String storageServiceUri;

    @GetMapping("/composition/{id}")
    publicMono<? extends ResponseEntity<? >> proxy(@PathVariableInteger id, ProxyExchange<? > proxy) {return proxy.uri(orderServiceUri + "/api/order/get/" + id)
            .get(resp -> ResponseEntity.status(resp.getStatusCode())
                .body(resp.getBody()))
            .flatMap(re1 -> proxy.uri(storageServiceUri + "/api/storage/get/" + id)
                .get(resp -> ResponseEntity.status(resp.getStatusCode())
                    .body(Map.of("order",re1.getBody(),"storage",resp.getBody())))); }}Copy the code

Yaml, set the URI to the Stub address, and obtain the random port by using ${wiremock.server.port}.

account-service-uri: http://localhost:${wiremock.server.port}
order-service-uri: http://localhost:${wiremock.server.port}
storage-service-uri: http://localhost:${wiremock.server.port}
Copy the code

In addition, the application – test. Need to make the following configuration in the yaml, first of all, we don’t need to pass the Token to Stub, so will the spring. Cloud. Gateway. The default – filters set to empty. In addition, you need to register a client named Test, where no real configuration information is required.

spring:
  cloud:
    gateway:
      default-filters: # set to empty
  security:
    oauth2:
      client:
        provider:
          test: # add for testing
            issuerUri: https://cognito-idp.<region-id>.amazonaws.com/<region-id>_<user-pool-id>
            user-name-attribute: username
        registration:
          test:
            client-id: dummy-client-id
            client-secret: dummy-client-secret
            client-name: scg-cognito-sample-user-pool
            provider: cognito
            scope: openid
            redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
            authorization-grant-type: authorization_code
Copy the code

We can then set up test stubs in the test class. The Stub below will return our default Header and Body when we access http://localhost:${wiremock.server.port}/api/storage/get/123.

@BeforeAll
static void init() {
    //Stubs
    stubFor(get(urlEqualTo("/api/storage/get/123"))
        .willReturn(aResponse()
            .withBody("{"id":101,"commodityCode":"123","count":100}")
            .withHeader("Content-Type", "application/json")));
    // ...
}
Copy the code

After starting the test application context, we make an authenticated request to the API gateway. We can use annotations like @WithMockUser(Roles = “ADMIN”) or the mutateWith method, which can set any properties we need. In the following example, we use the mutateWith method to construct an ID Token that allows OAuth authentication with the API gateway. If we do not use the mutateWith method, the API gateway returns 401 Unauthorized access.

@Test
void testGetComposition(a) {
    client.mutateWith(mockOidcLogin()
            .idToken(builder -> builder.subject("Subject A")))
        .get().uri("/composition/123").exchange()
        .expectStatus().is2xxSuccessful();
}
Copy the code

We can also add any Authority to the test user. As shown below, we added role_account. access to API gateway so that it can pass hasRole(“account.access”) authentication. Otherwise, the API gateway returns 403 Forbidden.

@Test
void testGetHomeAuthenticated(a) {

    client.mutateWith(mockOidcLogin()
            .idToken(builder -> builder.subject("Subject A"))
            .authorities(new SimpleGrantedAuthority("ROLE_account.access"))
        )
        .get().uri("/api/account/whoami").exchange()
        .expectStatus().is2xxSuccessful()
        .expectBody().jsonPath("$.['account.access']").isEqualTo("/api/account/**");
}
Copy the code

Testing of microservices

Testing for the back-end microservice is relatively simple, and we just need to validate the JWT token on the microservice. We used the new JWT () RequestPostProcessor introduced in Spring Security 5.2 in the Account service to easily change the JWT features.

@SpringBootTest
@AutoConfigureMockMvc
class AccountControllerTest {

    @Autowired
    private MockMvc mockmvc;

    private final String base_url = "/api/account";

    @Test
    void whoami(a) throws Exception {
        mockmvc.perform(get(base_url + "/whoami").with(jwt().jwt(builder -> builder.subject("Subject A"))))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("Subject A")); }}Copy the code

conclusion

This article focuses on the singleton/integration testing method for microservices, using WireMock to set up the test stakes, and using mutateWith and JWT () to set up the various information required for OAuth certification. If it helps you, please click “like” and subscribe to share. Thanks!

Related articles

  • Microservices: BFF case based on Spring Cloud Gateway + AWS Cognito

Refer to the link

  • Building a Gateway
  • Reactive Test Support