Spring security teste de integração

Fala pessoal! Nesse post mostramos como fazer um teste de integração da forma mais simples possível. Em sequida fizemos várias implementações no mesmo projeto. Implementamos autenticação e autorização com Spring security e JWT. Criamos um emdpoint público /products e outro endpoint privado /caretgory. O endpoint privado tem vários métodos para inserir, alterar, buscar e excluir registro do domínio Category. 

Agora vamos fazer duas classes de teste de integração que vão testar os endpoints desses dois domínios. Vamos usar o projeto feito nesse post

Primeiramente vamos alterar o endpoint /products para product em ProductResource. No findById alterar o endpoint para receber apenas o id do produto e excluir o endpoint /test

      package com.br.davifelipe.springjwt.resources;
     import org.modelmapper.ModelMapper;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.http.ResponseEntity;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.PathVariable;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RestController;
     import com.br.davifelipe.springjwt.dto.ProductDTO;
      import com.br.davifelipe.springjwt.model.Product;
      import com.br.davifelipe.springjwt.services.ProductService;
      import com.br.davifelipe.springjwt.services.exceptions.ObjectNotFoundException;
     @RestController
      @RequestMapping("/product")
      public class ProductResoruce {
          
          @Autowired
          private ProductService service;
          
          @GetMapping("/{id}")
          public ResponseEntity<?> findById(@PathVariable(value="id") String id) {
              
              ModelMapper modelMapper = new ModelMapper();
              
              Product product = service.findByid(Integer.parseInt(id));
              
              if(product == null) {
                  throw new ObjectNotFoundException("Object Product not found! id "+id);
              }
              
              ProductDTO productDTO = modelMapper.map(product,ProductDTO.class);
              
              return ResponseEntity.ok().body(productDTO);
          }
          
      }
      

Em seguida temos que autlizar esse endpoint alterado em ScurityConfig

      package com.br.davifelipe.springjwt.config;
     import java.util.ArrayList;
      import java.util.Arrays;
      import java.util.List;
     import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.beans.factory.annotation.Value;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.core.env.Environment;
      import org.springframework.http.HttpMethod;
      import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
      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.configuration.WebSecurityConfigurerAdapter;
      import org.springframework.security.config.http.SessionCreationPolicy;
      import org.springframework.security.core.userdetails.UserDetailsService;
      import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
      import org.springframework.web.cors.CorsConfiguration;
      import org.springframework.web.cors.CorsConfigurationSource;
      import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
     import com.br.davifelipe.springjwt.security.JWTAuthenticationFilter;
      import com.br.davifelipe.springjwt.security.JWTAuthorizationFilter;
     @Configuration
      @EnableWebSecurity
      public class SecurityConfig extends WebSecurityConfigurerAdapter {
          
          public static final List<String> PUBLIC_MATCHERS = new ArrayList<String>();
          public static final List<String> PUBLIC_MATCHERS_GET = new ArrayList<String>();
          public static final List<String> PUBLIC_MATCHERS_POST = new ArrayList<String>();
          
          @Autowired
          JWTUtil jwtUtil;
          
          @Autowired
          UserDetailsService userDetailsService;
          
          @Autowired
          private Environment env;
          
          @Value("${auth.public-sing-up-url-enable}")
          private String publicSingUpUrlEnable;
          
          @Override
          protected void configure(HttpSecurity http) throws Exception {
              
              PUBLIC_MATCHERS.add("/h2-console/**");
              PUBLIC_MATCHERS_GET.add("/product/**");
              
              String[] activeProfiles = env.getActiveProfiles();
              
              if (Arrays.asList(activeProfiles).contains("dev")) {
                  //disable it only for h2-console on dev envioment
                  http.headers().frameOptions().disable();
              }
              
              if("true".equals(this.publicSingUpUrlEnable)) {
                  PUBLIC_MATCHERS_POST.add("/auth/sing-up/**");
              }
              
              http.cors().and().csrf().disable();
              http.authorizeRequests()
                  .antMatchers(PUBLIC_MATCHERS.toArray(new String[0])).permitAll()
                  .antMatchers(HttpMethod.POST, PUBLIC_MATCHERS_POST.toArray(new String[0])).permitAll()
                  .antMatchers(HttpMethod.GET, PUBLIC_MATCHERS_GET.toArray(new String[0])).permitAll()
                  .anyRequest()
                  .authenticated();
              http.addFilter(new JWTAuthenticationFilter(authenticationManager(), jwtUtil));
              http.addFilter(new JWTAuthorizationFilter(authenticationManager(), jwtUtil, userDetailsService));
              http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
          }
          
          @Override
          public void configure(AuthenticationManagerBuilder auth) throws Exception {
              auth.userDetailsService(userDetailsService).passwordEncoder(bcCryptPasswordEncoder());
          }
          
          @Bean
          CorsConfigurationSource configurationSource() {
              CorsConfiguration configuration = new CorsConfiguration().applyPermitDefaultValues();
              configuration.setAllowedMethods(Arrays.asList("POST", "GET", "PUT", "DELETE", "OPTIONS"));
              final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
              source.registerCorsConfiguration("/**", configuration);
              return source;
          }
          
          @Bean
          public BCryptPasswordEncoder bcCryptPasswordEncoder() {
              return new BCryptPasswordEncoder();
          }
      }
      

E finalmente criar o teste de integração para o endpoint /product

      package com.br.davifelipe.springjwt.resources;
     import static io.restassured.RestAssured.given;
      import static org.junit.jupiter.api.Assertions.assertEquals;
     import java.math.BigDecimal;
     import org.junit.jupiter.api.BeforeAll;
      import org.junit.jupiter.api.DisplayName;
      import org.junit.jupiter.api.Test;
      import org.junit.jupiter.api.TestInstance;
      import org.junit.jupiter.api.TestInstance.Lifecycle;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
      import org.springframework.boot.web.server.LocalServerPort;
     import com.br.davifelipe.springjwt.dto.ProductDTO;
      import com.br.davifelipe.springjwt.model.Product;
      import com.br.davifelipe.springjwt.repositories.ProductRepository;
     @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
      @TestInstance(Lifecycle.PER_CLASS)
      public class ProductResourceTest{
          
          @LocalServerPort
          private int port;
          
          @Autowired
          private ProductRepository repositoryProduct;
          
          private Product insertedProduct;
          
          @BeforeAll
          public void prepare() {
              
              this.insertedProduct = new Product();
              this.insertedProduct.setName("Mouse");
              this.insertedProduct.setPrice(new BigDecimal(4.5));
              this.insertedProduct = this.repositoryProduct.saveAndFlush(this.insertedProduct);
          }
          
          @Test
          @DisplayName("Get product by ID [GET]")
          public void getProductById() {
              
              ProductDTO productDTO = given()
                                .contentType("application/json")
                                .port(port)
                                .when().get("/product/"+this.insertedProduct.getId())
                                .then().statusCode(200)
                                .extract().as(ProductDTO.class);
              
              assertEquals(this.insertedProduct.getName(), productDTO.getName());
         }
          
      }
      

A anotação @BeforeAll é do Junit5 ela faz o mesmo efeito da anotação @BeforeClass do Junit4. Por padrão, o método BeforeAll deve ser estático. Mas nesse caso adicionmos a anotação @TestInstance(Lifecycle.PER_CLASS) para que o cículo de vida do teste seja por classe. O que não nos obriga mais a usar esse método estático.

Fizemos isso por que adicionamos um registro em BeforeAll e precisamos esse objeto no teste. o método saveAndFlush chamado direto pelo repositório foi necessário por que precisamos que o registro já esteja disponível no banco de dados quando o teste for executado. Caso contrário, o registro só estaria disponível no banco de dados depois que finalizasse a sessão.

Agora basta rodar o teste e conferir se está tudo certo

Imagem

Já para o endpoint categoria o teste fica mais interessante por que antes testar temos que fazer a autenticação e passar o token nas requisições em que os endpoints estão protegitos com autenticação.

Nesse caso vamos utilizar uma anotação @Order para garantir que os testes serão executados na ordem do meu plano de teste. Pois vamos primeiro fazer uma requisição de autenticação para pegar o token e usar esse mesmo token para todas as requisições que exigem autenticação

  1. Fazemos um requisição para um endpoint protegido e verificamos se retorna 403 (Não autorizado)
  2. Cadastramos um novo usuário pelo endpoint /sing-up feito nesse post
  3. Com o usuário já cadastrado, fazermos uma requisição de autenticação pra o endpoint /login (mais detalhes nesse post) e armazenamos o token na variável global token que será utilizado nos outros testes.
  4. Agora que já temos o token, fazemos uma requisição de busca passando o token. Nesse caso estamos verificando se a autenticação funcionou e também nos certificamos que retorna 404 quando não há registro cadastrado.
  5. Testamos se o endpoint /category [POST] insert está funcionando. Nesse caso, já pegamos a url para consultar o retistro inserido, extraimos o id dessa url e adicionamos o id no objeto por que vamos precisar do id no próximo teste Update.
  6. Antes de testar update, vamos testar novamente o find (passando o id que foi inserido) e verificar se dessa vez o registro é encontrado.
  7. Testamos o update pelo endpoint /category/{id} [PUT]
  8. Em seguida vamos verificar se o resitro foi realmente alterado
  9. Testamos /caregory/{id} [DELTE]
  10. E finalmente chamamos o find para verificar se o registro foi realmente excluído.
      package com.br.davifelipe.springjwt.resources;
      import static io.restassured.RestAssured.given;
      import static org.junit.jupiter.api.Assertions.assertEquals;
      import org.junit.jupiter.api.BeforeAll;
      import org.junit.jupiter.api.DisplayName;
      import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
      import org.junit.jupiter.api.Order;
      import org.junit.jupiter.api.Test;
      import org.junit.jupiter.api.TestInstance;
      import org.junit.jupiter.api.TestInstance.Lifecycle;
      import org.junit.jupiter.api.TestMethodOrder;
      import org.modelmapper.ModelMapper;
      import org.springframework.boot.test.context.SpringBootTest;
      import org.springframework.boot.web.server.LocalServerPort;
      import org.springframework.test.context.TestPropertySource;
      import com.br.davifelipe.springjwt.dto.CategoryDTO;
      import com.br.davifelipe.springjwt.dto.SingInDTO;
      import com.br.davifelipe.springjwt.dto.SingUpDTO;
      import com.br.davifelipe.springjwt.model.User;
      @TestPropertySource("file:src/test/resources/application.properties")
      @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
      @TestInstance(Lifecycle.PER_CLASS)
      @TestMethodOrder(OrderAnnotation.class)
      class CaretoryResourceTest {
          
          @LocalServerPort
          private int port;
          
          private String token;
          
          private User userMock;
          
          private CategoryDTO categoryDTOMock;
          
          @BeforeAll
          public void prepare() {
              this.userMock = new User();
              this.categoryDTOMock = new CategoryDTO();
              
              this.userMock.setEmail("test@test.com");
              this.userMock.setName("User test");
              this.userMock.setPassword("123456");
              
              this.categoryDTOMock.setName("Devices");
          }
          
          @Test
          @DisplayName("Category not authorized [GET]")
          @Order(1)
          void notAutorized() {
                  given()
                  .contentType("application/json")
                  .port(port)
                  .when().get("/category/1")
                  .then().statusCode(403);
          }
          
          @Test
          @DisplayName("Sing Up [POST]")
          @Order(2)
          void singUp() {
              
              ModelMapper modelMapper = new ModelMapper();
              SingUpDTO singUpDTO = modelMapper.map(this.userMock,SingUpDTO.class);
              
              given()
              .contentType("application/json")
              .body(singUpDTO)
              .port(port)
              .when().post("/auth/sing-up")
              .then().statusCode(201);
              
          }
          
          @Test
          @DisplayName("Sing in -> Get Token [POST]")
          @Order(3)
          void getToken() {
              
              ModelMapper modelMapper = new ModelMapper();
              SingInDTO singInDTO = modelMapper.map(this.userMock,SingInDTO.class);
              
              this.token =    given()
                              .contentType("application/json")
                              .body(singInDTO)
                              .port(port)
                              .when().post("/login")
                              .then().statusCode(200).extract().header("Authorization");
              
              
          }
          
          private void categoryNotFound() {
              given()
              .header("Authorization", this.token)
              .contentType("application/json")
              .port(port)
              .when().get("/category/1")
              .then().statusCode(404);
          }
          
          @Test
          @DisplayName("Category no found [GET]")
          @Order(4)
          void notFoundCategory() {
              this.categoryNotFound();
          }
          
          @Test
          @DisplayName("Category insert [POST]")
          @Order(5)
          void insertCategory() {
              
              this.categoryDTOMock.setId(null);
              
              String categorySavedUrl = given()
                                      .header("Authorization", this.token)
                                      .contentType("application/json")
                                      .body(this.categoryDTOMock)
                                      .port(port)
                                      .when().post("/category")
                                      .then().statusCode(201).extract().header("Location");
              
              String splitedUrl[] = categorySavedUrl.split("/");
              this.categoryDTOMock.setId(Integer.parseInt(splitedUrl[splitedUrl.length -1]));
          }
          
          @Test
          @DisplayName("Category found [GET]")
          @Order(6)
          void foundCategory() {
              CategoryDTO caretoryDTORetrived =    given()
                      .header("Authorization", this.token)
                      .contentType("application/json")
                      .port(port)
                      .when().get("/category/"+this.categoryDTOMock.getId())
                      .then().statusCode(200)
                      .extract().as(CategoryDTO.class);
             assertEquals(this.categoryDTOMock.getName(), caretoryDTORetrived.getName());
          }
          
          @Test
          @DisplayName("Category update [PUT]")
          @Order(7)
          void updateCategory() {
              
              this.categoryDTOMock.setName("Device Updated!");
               given()
              .header("Authorization", this.token)
              .contentType("application/json")
              .body(this.categoryDTOMock)
              .port(port)
              .when().put("/category/"+this.categoryDTOMock.getId())
              .then().statusCode(204);
          }
          
          @Test
          @DisplayName("Category check if it was updated [GET]")
          @Order(8)
          void updateCheckCategory() {
              CategoryDTO caretoryDTORetrived =    given()
                                          .header("Authorization", this.token)
                                          .contentType("application/json")
                                          .port(port)
                                          .when().get("/category/"+this.categoryDTOMock.getId())
                                          .then().statusCode(200)
                                          .extract().as(CategoryDTO.class);
              
              assertEquals(this.categoryDTOMock.getName(), caretoryDTORetrived.getName());
          }
          
          @Test
          @DisplayName("Category delte [DELETE]")
          @Order(9)
          void deleteCategory() {
              
               given()
              .header("Authorization", this.token)
              .contentType("application/json")
              .port(port)
              .when().delete("/category/"+this.categoryDTOMock.getId())
              .then().statusCode(204);
          }
          
          @Test
          @DisplayName("Category check if it was deleted [GET]")
          @Order(10)
          void delteCheckCategory() {
              this.categoryNotFound();
          }
          
      }
      

Utilizamos essa notação TestPropertySource para informar que vamos carregar um arquivo de configuração específico para os testes de integração.

Esse arquivo foi criado em /src/test/resources

      spring.profiles.active = test
     spring.h2.console.enabled=true
      spring.h2.console.path=/h2-console
      spring.datasource.url=jdbc:h2:file:~/test
      spring.datasource.username=sa
      spring.datasource.password=
      spring.datasource.driver-class-name=org.h2.Driver
      spring.jpa.show-sql=true
      spring.jpa.properties.hibernate.format_sql=true
     auth.public-sing-up-url-enable=true
      auth.jwt-secret=MySecretJwtWord
      auth.jwt-expiration-miliseg=86400000

Finalmente vamos rodar os testes e ver o resultado

Imagem

Repositório do projeto https://github.com/davifelipems/spring-backend-template/tree/jwt_integration_test

Comentários

 

Quem Sou

Graduado em ADS (Análise e desenvolvimento de sistemas).

Não sou "devoto" de nenhuma linguagem de programação. Procuro aproveitar o melhor de cada uma de acordo com a necessidade do projeto. Prezo por uma arquitetura bem feita, código limpo, puro e simples! 

anuncio atendente