diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/application/clients/IbgeClient.java b/pointtils/src/main/java/com/pointtils/pointtils/src/application/clients/IbgeClient.java new file mode 100644 index 00000000..57baeeaf --- /dev/null +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/application/clients/IbgeClient.java @@ -0,0 +1,73 @@ +package com.pointtils.pointtils.src.application.clients; + +import com.pointtils.pointtils.src.application.dto.CityIbgeResponseDTO; +import com.pointtils.pointtils.src.application.dto.StateDataDTO; +import com.pointtils.pointtils.src.application.dto.StateIbgeResponseDTO; +import com.pointtils.pointtils.src.core.domain.exceptions.ClientTimeoutException; +import jakarta.persistence.EntityNotFoundException; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestTemplate; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.function.Function; + +@Slf4j +@Component +public class IbgeClient { + + private final RestTemplate restTemplate; + private final String stateUrl; + private final String cityUrl; + + public IbgeClient(@Qualifier("ibgeRestTemplate") RestTemplate restTemplate, + @Value("${client.ibge.state-url}") String stateUrl, + @Value("${client.ibge.city-url}") String cityUrl) { + this.restTemplate = restTemplate; + this.stateUrl = stateUrl; + this.cityUrl = cityUrl; + } + + public List getStateList() { + try { + log.info("Iniciando uma requisicao para a API do IBGE para buscar a lista de UFs"); + ResponseEntity stateResponse = restTemplate.getForEntity(stateUrl, StateIbgeResponseDTO[].class); + Function mapper = state -> new StateDataDTO(state.getAbbreviation()); + return formatResponseData(stateResponse.getBody(), mapper, "UFs não encontradas"); + } catch (ResourceAccessException ex) { + log.error("Timeout na requisicao para a API do IBGE para buscar a lista de UFs", ex); + throw new ClientTimeoutException("Timeout ao acessar o serviço de UFs"); + } + } + + public List getCityListByState(String state) { + try { + log.info("Iniciando uma requisicao para a API do IBGE para buscar a lista de municípios da UF {}", state); + ResponseEntity cityResponse = restTemplate.getForEntity(cityUrl, CityIbgeResponseDTO[].class, state); + Function mapper = city -> new StateDataDTO(city.getName()); + return formatResponseData(cityResponse.getBody(), mapper, "Municípios não encontrados"); + } catch (ResourceAccessException ex) { + log.error("Timeout na requisicao para a API do IBGE para buscar a lista de municípios da UF {}", state, ex); + throw new ClientTimeoutException("Timeout ao acessar o serviço de municípios"); + } + } + + private List formatResponseData(T[] responseBody, + Function mapper, + String errorMessage) { + if (ArrayUtils.isEmpty(responseBody)) { + throw new EntityNotFoundException(errorMessage); + } + return Arrays.stream(responseBody) + .map(mapper) + .sorted(Comparator.comparing(StateDataDTO::getName)) + .toList(); + } +} diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/application/controllers/StateController.java b/pointtils/src/main/java/com/pointtils/pointtils/src/application/controllers/StateController.java new file mode 100644 index 00000000..cbd8c228 --- /dev/null +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/application/controllers/StateController.java @@ -0,0 +1,38 @@ +package com.pointtils.pointtils.src.application.controllers; + +import com.pointtils.pointtils.src.application.dto.StateResponseDTO; +import com.pointtils.pointtils.src.application.services.StateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +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.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/states") +@SecurityRequirement(name = "bearerAuth") +@Tag(name = "State Controller", description = "Endpoints para gerenciar dados de UFs brasileiras") +public class StateController { + + private final StateService stateService; + + @GetMapping + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "Busca todas as UFs brasileiras") + public StateResponseDTO getStates() { + return stateService.getAllStates(); + } + + @GetMapping("/{stateId}/cities") + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "Busca todos os municípios de uma determinada UF brasileira") + public StateResponseDTO getCitiesByState(@PathVariable String stateId) { + return stateService.getCitiesByState(stateId); + } +} diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/CityIbgeResponseDTO.java b/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/CityIbgeResponseDTO.java new file mode 100644 index 00000000..5963dd68 --- /dev/null +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/CityIbgeResponseDTO.java @@ -0,0 +1,19 @@ +package com.pointtils.pointtils.src.application.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class CityIbgeResponseDTO { + + private Long id; + + @JsonProperty("nome") + private String name; +} diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/StateDataDTO.java b/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/StateDataDTO.java new file mode 100644 index 00000000..cf305877 --- /dev/null +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/StateDataDTO.java @@ -0,0 +1,15 @@ +package com.pointtils.pointtils.src.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class StateDataDTO { + + private String name; +} diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/StateIbgeResponseDTO.java b/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/StateIbgeResponseDTO.java new file mode 100644 index 00000000..09c810c5 --- /dev/null +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/StateIbgeResponseDTO.java @@ -0,0 +1,22 @@ +package com.pointtils.pointtils.src.application.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class StateIbgeResponseDTO { + + private Long id; + + @JsonProperty("sigla") + private String abbreviation; + + @JsonProperty("nome") + private String name; +} diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/StateResponseDTO.java b/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/StateResponseDTO.java new file mode 100644 index 00000000..7e6a92c8 --- /dev/null +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/StateResponseDTO.java @@ -0,0 +1,19 @@ +package com.pointtils.pointtils.src.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class StateResponseDTO { + + private boolean success; + private String message; + private List data; +} diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/application/services/AuthService.java b/pointtils/src/main/java/com/pointtils/pointtils/src/application/services/AuthService.java index e960f90e..b85f0b16 100644 --- a/pointtils/src/main/java/com/pointtils/pointtils/src/application/services/AuthService.java +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/application/services/AuthService.java @@ -1,7 +1,5 @@ package com.pointtils.pointtils.src.application.services; -import java.util.UUID; - import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/application/services/StateService.java b/pointtils/src/main/java/com/pointtils/pointtils/src/application/services/StateService.java new file mode 100644 index 00000000..d7408054 --- /dev/null +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/application/services/StateService.java @@ -0,0 +1,22 @@ +package com.pointtils.pointtils.src.application.services; + +import com.pointtils.pointtils.src.application.clients.IbgeClient; +import com.pointtils.pointtils.src.application.dto.StateResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class StateService { + + private final IbgeClient ibgeClient; + + public StateResponseDTO getAllStates() { + return new StateResponseDTO(true, "UFs encontradas com sucesso", ibgeClient.getStateList()); + } + + public StateResponseDTO getCitiesByState(String state) { + return new StateResponseDTO(true, "Municípios encontrados com sucesso", + ibgeClient.getCityListByState(state)); + } +} diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/core/domain/entities/User.java b/pointtils/src/main/java/com/pointtils/pointtils/src/core/domain/entities/User.java index 4187b139..9751cc31 100644 --- a/pointtils/src/main/java/com/pointtils/pointtils/src/core/domain/entities/User.java +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/core/domain/entities/User.java @@ -1,7 +1,5 @@ package com.pointtils.pointtils.src.core.domain.entities; -import java.util.UUID; - import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -11,6 +9,8 @@ import jakarta.persistence.Table; import org.hibernate.annotations.UuidGenerator; +import java.util.UUID; + @Entity @Inheritance(strategy = InheritanceType.JOINED) @Table(name = "user_account") @@ -32,18 +32,54 @@ public abstract class User { private String status; public abstract String getDisplayName(); + public abstract String getType(); - public UUID getId() { return id; } - public String getEmail() { return email; } - public String getPassword() { return password; } - public String getPhone() { return phone; } - public String getPicture() { return picture; } - public String getStatus() { return status; } - public void setId(UUID id) { this.id = id; } - public void setEmail(String email) { this.email = email; } - public void setPassword(String password) { this.password = password; } - public void setPhone(String phone) { this.phone = phone; } - public void setPicture(String picture) { this.picture = picture; } - public void setStatus(String status) { this.status = status; } + public UUID getId() { + return id; + } + + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } + + public String getPhone() { + return phone; + } + + public String getPicture() { + return picture; + } + + public String getStatus() { + return status; + } + + public void setId(UUID id) { + this.id = id; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public void setPicture(String picture) { + this.picture = picture; + } + + public void setStatus(String status) { + this.status = status; + } } diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/core/domain/exceptions/ClientTimeoutException.java b/pointtils/src/main/java/com/pointtils/pointtils/src/core/domain/exceptions/ClientTimeoutException.java new file mode 100644 index 00000000..a0a61061 --- /dev/null +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/core/domain/exceptions/ClientTimeoutException.java @@ -0,0 +1,7 @@ +package com.pointtils.pointtils.src.core.domain.exceptions; + +public class ClientTimeoutException extends RuntimeException { + public ClientTimeoutException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/GlobalExceptionHandler.java b/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/GlobalExceptionHandler.java index 542e2f66..ce75b7b6 100644 --- a/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/GlobalExceptionHandler.java +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/GlobalExceptionHandler.java @@ -1,18 +1,17 @@ package com.pointtils.pointtils.src.infrastructure.configs; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.context.request.WebRequest; - import com.pointtils.pointtils.src.core.domain.exceptions.AuthenticationException; +import com.pointtils.pointtils.src.core.domain.exceptions.ClientTimeoutException; import com.pointtils.pointtils.src.core.domain.exceptions.UserSpecialtyException; - import jakarta.persistence.EntityNotFoundException; import lombok.AllArgsConstructor; import lombok.Data; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; @ControllerAdvice public class GlobalExceptionHandler { @@ -20,24 +19,24 @@ public class GlobalExceptionHandler { @ExceptionHandler(EntityNotFoundException.class) public ResponseEntity handleEntityNotFoundException( EntityNotFoundException ex, WebRequest request) { - + ErrorResponse errorResponse = new ErrorResponse( HttpStatus.NOT_FOUND.value(), ex.getMessage(), System.currentTimeMillis()); - + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); } @ExceptionHandler(Exception.class) public ResponseEntity handleGlobalException( Exception ex, WebRequest request) { - + ErrorResponse errorResponse = new ErrorResponse( HttpStatus.INTERNAL_SERVER_ERROR.value(), "An unexpected error occurred", System.currentTimeMillis()); - + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); } @@ -108,6 +107,16 @@ public ResponseEntity handleAuthentication(AuthenticationExceptio return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); } + @ExceptionHandler(ClientTimeoutException.class) + public ResponseEntity handleClientTimeoutException(ClientTimeoutException ex) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.GATEWAY_TIMEOUT.value(), + ex.getMessage(), + System.currentTimeMillis()); + + return new ResponseEntity<>(errorResponse, HttpStatus.GATEWAY_TIMEOUT); + } + @ExceptionHandler(UserSpecialtyException.class) public ResponseEntity handleUserSpecialty(UserSpecialtyException ex) { ErrorResponse errorResponse = new ErrorResponse( diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/OpenApiConfig.java b/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/OpenApiConfig.java index f6bb466d..de317e9c 100644 --- a/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/OpenApiConfig.java +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/OpenApiConfig.java @@ -1,13 +1,20 @@ package com.pointtils.pointtils.src.infrastructure.configs; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.security.SecurityScheme; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +@SecurityScheme( + name = "bearerAuth", + type = SecuritySchemeType.HTTP, + bearerFormat = "JWT", + scheme = "bearer" +) @Configuration public class OpenApiConfig { diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/RestTemplateConfig.java b/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/RestTemplateConfig.java new file mode 100644 index 00000000..dd1bb0d6 --- /dev/null +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/RestTemplateConfig.java @@ -0,0 +1,27 @@ +package com.pointtils.pointtils.src.infrastructure.configs; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Value("${client.ibge.timeout:3000}") + private Integer ibgeTimeoutInMs; + + @Bean + public RestTemplate ibgeRestTemplate() { + return new RestTemplate(buildClientHttpRequestFactory(ibgeTimeoutInMs)); + } + + private ClientHttpRequestFactory buildClientHttpRequestFactory(Integer timeoutInMs) { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(timeoutInMs); + factory.setReadTimeout(timeoutInMs); + return factory; + } +} diff --git a/pointtils/src/main/resources/application.properties b/pointtils/src/main/resources/application.properties index 22e192f7..293a1536 100644 --- a/pointtils/src/main/resources/application.properties +++ b/pointtils/src/main/resources/application.properties @@ -23,3 +23,7 @@ spring.flyway.validate-on-migrate=${SPRING_FLYWAY_VALIDATE_ON_MIGRATE} springdoc.api-docs.enabled=${SPRINGDOC_API_DOCS_ENABLED} springdoc.swagger-ui.enabled=${SPRINGDOC_SWAGGER_UI_ENABLED} springdoc.swagger-ui.path=${SPRINGDOC_SWAGGER_UI_PATH} + +# Clientes HTTP +client.ibge.state-url=https://servicodados.ibge.gov.br/api/v1/localidades/estados +client.ibge.city-url=https://servicodados.ibge.gov.br/api/v1/localidades/estados/{state}/municipios \ No newline at end of file diff --git a/pointtils/src/test/java/com/pointtils/pointtils/src/application/clients/IbgeClientTest.java b/pointtils/src/test/java/com/pointtils/pointtils/src/application/clients/IbgeClientTest.java new file mode 100644 index 00000000..7aa88a65 --- /dev/null +++ b/pointtils/src/test/java/com/pointtils/pointtils/src/application/clients/IbgeClientTest.java @@ -0,0 +1,117 @@ +package com.pointtils.pointtils.src.application.clients; + +import com.pointtils.pointtils.src.application.dto.CityIbgeResponseDTO; +import com.pointtils.pointtils.src.application.dto.StateIbgeResponseDTO; +import com.pointtils.pointtils.src.core.domain.exceptions.ClientTimeoutException; +import jakarta.persistence.EntityNotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestTemplate; + +import java.net.SocketTimeoutException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class IbgeClientTest { + + @Mock + private RestTemplate restTemplate; + private IbgeClient ibgeClient; + + @BeforeEach + void setUp() { + this.ibgeClient = new IbgeClient(restTemplate, "http://exemplo.com.br/estados", + "http://exemplo.com.br/estados/{state}/municipios"); + } + + @Test + @DisplayName("Deve obter da API externa a lista de UFs em ordem alfabética") + void shouldGetStateListInAlphabeticalOrder() { + var mockRsResponse = new StateIbgeResponseDTO(1L, "RS", "Rio Grande do Sul"); + var mockAcreResponse = new StateIbgeResponseDTO(2L, "AC", "Acre"); + when(restTemplate.getForEntity("http://exemplo.com.br/estados", StateIbgeResponseDTO[].class)) + .thenReturn(ResponseEntity.ok(new StateIbgeResponseDTO[]{mockRsResponse, mockAcreResponse})); + + var actualResponse = ibgeClient.getStateList(); + assertEquals(2, actualResponse.size()); + assertEquals("AC", actualResponse.get(0).getName()); + assertEquals("RS", actualResponse.get(1).getName()); + } + + @Test + @DisplayName("Deve lancar EntityNotFoundException se API externa retornar lista de UFs nula") + void shouldGetEntityNotFoundExceptionIfStateListIsNull() { + when(restTemplate.getForEntity("http://exemplo.com.br/estados", StateIbgeResponseDTO[].class)) + .thenReturn(ResponseEntity.ok(null)); + + assertThrows(EntityNotFoundException.class, () -> ibgeClient.getStateList()); + } + + @Test + @DisplayName("Deve lancar EntityNotFoundException se API externa retornar lista de UFs vazia") + void shouldGetEntityNotFoundExceptionIfStateListIsEmpty() { + when(restTemplate.getForEntity("http://exemplo.com.br/estados", StateIbgeResponseDTO[].class)) + .thenReturn(ResponseEntity.ok(new StateIbgeResponseDTO[]{})); + + assertThrows(EntityNotFoundException.class, () -> ibgeClient.getStateList()); + } + + @Test + @DisplayName("Deve lancar ClientTimeoutException se ocorrer timeout em chamada da API externa para obter UFs") + void shouldGetClientTimeoutExceptionIfStateListRequestTimeout() { + when(restTemplate.getForEntity("http://exemplo.com.br/estados", StateIbgeResponseDTO[].class)) + .thenThrow(new ResourceAccessException("Erro", new SocketTimeoutException("Timeout"))); + + assertThrows(ClientTimeoutException.class, () -> ibgeClient.getStateList()); + } + + @Test + @DisplayName("Deve obter da API externa a lista de municípios por UF em ordem alfabética") + void shouldGetCityListInAlphabeticalOrder() { + var mockCityResponse1 = new CityIbgeResponseDTO(1L, "Porto Alegre"); + var mockCityResponse2 = new CityIbgeResponseDTO(2L, "Canoas"); + when(restTemplate.getForEntity("http://exemplo.com.br/estados/{state}/municipios", CityIbgeResponseDTO[].class, "RS")) + .thenReturn(ResponseEntity.ok(new CityIbgeResponseDTO[]{mockCityResponse1, mockCityResponse2})); + + var actualResponse = ibgeClient.getCityListByState("RS"); + assertEquals(2, actualResponse.size()); + assertEquals("Canoas", actualResponse.get(0).getName()); + assertEquals("Porto Alegre", actualResponse.get(1).getName()); + } + + @Test + @DisplayName("Deve lancar EntityNotFoundException se API externa retornar lista de municípios nula") + void shouldGetEntityNotFoundExceptionIfCityListIsNull() { + when(restTemplate.getForEntity("http://exemplo.com.br/estados/{state}/municipios", CityIbgeResponseDTO[].class, "RS")) + .thenReturn(ResponseEntity.ok(null)); + + assertThrows(EntityNotFoundException.class, () -> ibgeClient.getCityListByState("RS")); + } + + @Test + @DisplayName("Deve lancar EntityNotFoundException se API externa retornar lista de municípios vazia") + void shouldGetEntityNotFoundExceptionIfCityListIsEmpty() { + when(restTemplate.getForEntity("http://exemplo.com.br/estados/{state}/municipios", CityIbgeResponseDTO[].class, "RS")) + .thenReturn(ResponseEntity.ok(new CityIbgeResponseDTO[]{})); + + assertThrows(EntityNotFoundException.class, () -> ibgeClient.getCityListByState("RS")); + } + + @Test + @DisplayName("Deve lancar ClientTimeoutException se ocorrer timeout em chamada da API externa para obter municípios") + void shouldGetClientTimeoutExceptionIfCityListRequestTimeout() { + when(restTemplate.getForEntity("http://exemplo.com.br/estados/{state}/municipios", CityIbgeResponseDTO[].class, "RS")) + .thenThrow(new ResourceAccessException("Erro", new SocketTimeoutException("Timeout"))); + + assertThrows(ClientTimeoutException.class, () -> ibgeClient.getCityListByState("RS")); + } +} diff --git a/pointtils/src/test/java/com/pointtils/pointtils/src/application/controllers/StateControllerTest.java b/pointtils/src/test/java/com/pointtils/pointtils/src/application/controllers/StateControllerTest.java new file mode 100644 index 00000000..fb578abc --- /dev/null +++ b/pointtils/src/test/java/com/pointtils/pointtils/src/application/controllers/StateControllerTest.java @@ -0,0 +1,95 @@ +package com.pointtils.pointtils.src.application.controllers; + +import com.pointtils.pointtils.src.application.dto.StateDataDTO; +import com.pointtils.pointtils.src.application.dto.StateResponseDTO; +import com.pointtils.pointtils.src.application.services.StateService; +import com.pointtils.pointtils.src.infrastructure.configs.GlobalExceptionHandler; +import com.pointtils.pointtils.src.infrastructure.configs.JwtAuthenticationFilter; +import com.pointtils.pointtils.src.infrastructure.configs.JwtService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ActiveProfiles("test") +@SpringBootTest(classes = StateController.class) +@Import(JwtAuthenticationFilter.class) +class StateControllerTest { + + @MockitoBean + private JwtService jwtService; + @MockitoBean + private StateService stateService; + + @Autowired + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @Autowired + private StateController stateController; + + private MockMvc mockMvc; + + @BeforeEach + void setup() { + this.mockMvc = MockMvcBuilders.standaloneSetup(stateController) + .setControllerAdvice(new GlobalExceptionHandler()) + .apply(springSecurity(jwtAuthenticationFilter)) + .build(); + when(jwtService.isTokenExpired(anyString())).thenReturn(Boolean.FALSE); + } + + @Test + @DisplayName("Deve retornar 200 e a lista de estados ao chamar o endpoint /v1/states") + void shouldGetOkResponseForGetStatesEndpoint() throws Exception { + StateResponseDTO mockResponse = new StateResponseDTO(true, "Sucesso", + List.of(new StateDataDTO("RS"))); + + when(stateService.getAllStates()).thenReturn(mockResponse); + + mockMvc.perform(MockMvcRequestBuilders + .get("/v1/states") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer valid_token")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("Sucesso")) + .andExpect(jsonPath("$.data[0].name").value("RS")); + } + + @Test + @DisplayName("Deve retornar 200 e a lista de municípios ao chamar o endpoint /v1/states/{id}/cities") + void shouldGetOkResponseForGetCitiesByStateEndpoint() throws Exception { + StateResponseDTO mockResponse = new StateResponseDTO(true, "Sucesso", + List.of(new StateDataDTO("Porto Alegre"))); + + when(stateService.getCitiesByState("RS")).thenReturn(mockResponse); + + mockMvc.perform(MockMvcRequestBuilders + .get("/v1/states/RS/cities") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer valid_token")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("Sucesso")) + .andExpect(jsonPath("$.data[0].name").value("Porto Alegre")); + } +} diff --git a/pointtils/src/test/java/com/pointtils/pointtils/dto/RefreshTokenRequestDTOTest.java b/pointtils/src/test/java/com/pointtils/pointtils/src/application/dto/RefreshTokenRequestDTOTest.java similarity index 88% rename from pointtils/src/test/java/com/pointtils/pointtils/dto/RefreshTokenRequestDTOTest.java rename to pointtils/src/test/java/com/pointtils/pointtils/src/application/dto/RefreshTokenRequestDTOTest.java index db504835..6336f507 100644 --- a/pointtils/src/test/java/com/pointtils/pointtils/dto/RefreshTokenRequestDTOTest.java +++ b/pointtils/src/test/java/com/pointtils/pointtils/src/application/dto/RefreshTokenRequestDTOTest.java @@ -1,9 +1,12 @@ -package com.pointtils.pointtils.dto; +package com.pointtils.pointtils.src.application.dto; -import com.pointtils.pointtils.src.application.dto.RefreshTokenRequestDTO; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; class RefreshTokenRequestDTOTest { diff --git a/pointtils/src/test/java/com/pointtils/pointtils/dto/RefreshTokenResponseDTOTest.java b/pointtils/src/test/java/com/pointtils/pointtils/src/application/dto/RefreshTokenResponseDTOTest.java similarity index 97% rename from pointtils/src/test/java/com/pointtils/pointtils/dto/RefreshTokenResponseDTOTest.java rename to pointtils/src/test/java/com/pointtils/pointtils/src/application/dto/RefreshTokenResponseDTOTest.java index 202c183b..83dcf4bf 100644 --- a/pointtils/src/test/java/com/pointtils/pointtils/dto/RefreshTokenResponseDTOTest.java +++ b/pointtils/src/test/java/com/pointtils/pointtils/src/application/dto/RefreshTokenResponseDTOTest.java @@ -1,14 +1,12 @@ -package com.pointtils.pointtils.dto; +package com.pointtils.pointtils.src.application.dto; + +import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.api.Test; - -import com.pointtils.pointtils.src.application.dto.RefreshTokenResponseDTO; -import com.pointtils.pointtils.src.application.dto.TokensDTO; class RefreshTokenResponseDTOTest { diff --git a/pointtils/src/test/java/com/pointtils/pointtils/src/application/services/StateServiceTest.java b/pointtils/src/test/java/com/pointtils/pointtils/src/application/services/StateServiceTest.java new file mode 100644 index 00000000..6130e2dc --- /dev/null +++ b/pointtils/src/test/java/com/pointtils/pointtils/src/application/services/StateServiceTest.java @@ -0,0 +1,55 @@ +package com.pointtils.pointtils.src.application.services; + +import com.pointtils.pointtils.src.application.clients.IbgeClient; +import com.pointtils.pointtils.src.application.dto.StateDataDTO; +import org.junit.jupiter.api.DisplayName; +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 java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class StateServiceTest { + + @Mock + private IbgeClient ibgeClient; + @InjectMocks + private StateService stateService; + + @Test + @DisplayName("Deve buscar todas as UFs") + void shouldGetAllStates() { + var firstMockedData = new StateDataDTO("RS"); + var secondMockedData = new StateDataDTO("SC"); + when(ibgeClient.getStateList()).thenReturn(List.of(firstMockedData, secondMockedData)); + + var response = stateService.getAllStates(); + assertTrue(response.isSuccess()); + assertEquals("UFs encontradas com sucesso", response.getMessage()); + assertThat(response.getData()) + .hasSize(2) + .containsExactly(firstMockedData, secondMockedData); + } + + @Test + @DisplayName("Deve buscar todos os municípios de uma determinada UF") + void shouldGetCitiesByState() { + var firstMockedData = new StateDataDTO("Porto Alegre"); + when(ibgeClient.getCityListByState("RS")).thenReturn(List.of(firstMockedData)); + + var response = stateService.getCitiesByState("RS"); + assertTrue(response.isSuccess()); + assertEquals("Municípios encontrados com sucesso", response.getMessage()); + assertThat(response.getData()) + .hasSize(1) + .containsExactly(firstMockedData); + } +} diff --git a/pointtils/src/test/java/com/pointtils/pointtils/JwtAuthenticationFilterTest.java b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/configs/JwtAuthenticationFilterTest.java similarity index 93% rename from pointtils/src/test/java/com/pointtils/pointtils/JwtAuthenticationFilterTest.java rename to pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/configs/JwtAuthenticationFilterTest.java index 0558c439..048b35ba 100644 --- a/pointtils/src/test/java/com/pointtils/pointtils/JwtAuthenticationFilterTest.java +++ b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/configs/JwtAuthenticationFilterTest.java @@ -1,9 +1,6 @@ -package com.pointtils.pointtils; +package com.pointtils.pointtils.src.infrastructure.configs; -import com.pointtils.pointtils.src.infrastructure.configs.JwtAuthenticationFilter; -import com.pointtils.pointtils.src.infrastructure.configs.JwtService; import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.BeforeEach; @@ -14,16 +11,20 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.test.util.ReflectionTestUtils; -import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Method; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class JwtAuthenticationFilterTest { diff --git a/pointtils/src/test/java/com/pointtils/pointtils/JwtRefreshTokenTest.java b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/configs/JwtRefreshTokenTest.java similarity index 92% rename from pointtils/src/test/java/com/pointtils/pointtils/JwtRefreshTokenTest.java rename to pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/configs/JwtRefreshTokenTest.java index 6da907a8..085ad082 100644 --- a/pointtils/src/test/java/com/pointtils/pointtils/JwtRefreshTokenTest.java +++ b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/configs/JwtRefreshTokenTest.java @@ -1,14 +1,12 @@ -package com.pointtils.pointtils; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +package com.pointtils.pointtils.src.infrastructure.configs; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.TestPropertySource; -import com.pointtils.pointtils.src.infrastructure.configs.JwtService; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; @SpringBootTest @TestPropertySource(locations = "classpath:application.properties") diff --git a/pointtils/src/test/java/com/pointtils/pointtils/JwtServiceTest.java b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/configs/JwtServiceTest.java similarity index 92% rename from pointtils/src/test/java/com/pointtils/pointtils/JwtServiceTest.java rename to pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/configs/JwtServiceTest.java index 8184c841..666ddf38 100644 --- a/pointtils/src/test/java/com/pointtils/pointtils/JwtServiceTest.java +++ b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/configs/JwtServiceTest.java @@ -1,4 +1,4 @@ -package com.pointtils.pointtils; +package com.pointtils.pointtils.src.infrastructure.configs; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; @@ -7,11 +7,17 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.test.util.ReflectionTestUtils; -import com.pointtils.pointtils.src.infrastructure.configs.JwtService; import java.util.Date; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class JwtServiceTest { private JwtService jwtService; @@ -84,7 +90,7 @@ void shouldExtractIssuedAtFromToken() { // Then assertNotNull(issuedAt); long issuedAtTime = issuedAt.getTime(); - assertTrue(issuedAtTime >= beforeTokenGeneration, + assertTrue(issuedAtTime >= beforeTokenGeneration, "IssuedAt time " + issuedAtTime + " should be >= " + beforeTokenGeneration); assertTrue(issuedAtTime <= afterTokenGeneration, "IssuedAt time " + issuedAtTime + " should be <= " + afterTokenGeneration); @@ -140,7 +146,7 @@ void shouldDetectExpiredToken() { JwtService expiredJwtService = new JwtService(); ReflectionTestUtils.setField(expiredJwtService, "secretKey", TEST_SECRET_KEY); ReflectionTestUtils.setField(expiredJwtService, "jwtExpiration", -60000L); // Already expired (negative 1 minute) - + String expiredToken = expiredJwtService.generateToken("testuser"); // When & Then - Expect an ExpiredJwtException to be thrown when checking expired token @@ -166,7 +172,7 @@ void shouldThrowExceptionForTokenWithWrongSignature() { JwtService differentKeyService = new JwtService(); ReflectionTestUtils.setField(differentKeyService, "secretKey", "ZGlmZmVyZW50a2V5ZGlmZmVyZW50a2V5ZGlmZmVyZW50a2V5ZGlmZmVyZW50a2V5"); ReflectionTestUtils.setField(differentKeyService, "jwtExpiration", JWT_EXPIRATION); - + String tokenWithDifferentKey = differentKeyService.generateToken("testuser"); // When & Then diff --git a/pointtils/src/test/java/com/pointtils/pointtils/SecurityConfigurationTest.java b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/configs/SecurityConfigurationTest.java similarity index 95% rename from pointtils/src/test/java/com/pointtils/pointtils/SecurityConfigurationTest.java rename to pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/configs/SecurityConfigurationTest.java index 6eb817f2..0bf70506 100644 --- a/pointtils/src/test/java/com/pointtils/pointtils/SecurityConfigurationTest.java +++ b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/configs/SecurityConfigurationTest.java @@ -1,7 +1,5 @@ -package com.pointtils.pointtils; +package com.pointtils.pointtils.src.infrastructure.configs; -import com.pointtils.pointtils.src.infrastructure.configs.JwtAuthenticationFilter; -import com.pointtils.pointtils.src.infrastructure.configs.SecurityConfiguration; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -11,7 +9,10 @@ import java.lang.reflect.Method; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; @ExtendWith(MockitoExtension.class) class SecurityConfigurationTest { @@ -42,7 +43,7 @@ void corsConfigurationShouldAllowAllOrigins() throws Exception { Method corsConfigurationSourceMethod = SecurityConfiguration.class.getDeclaredMethod("corsConfigurationSource"); corsConfigurationSourceMethod.setAccessible(true); CorsConfigurationSource corsConfigurationSource = (CorsConfigurationSource) corsConfigurationSourceMethod.invoke(securityConfiguration); - + MockHttpServletRequest request = new MockHttpServletRequest(); var corsConfiguration = corsConfigurationSource.getCorsConfiguration(request); @@ -61,7 +62,7 @@ void corsConfigurationShouldAllowExpectedMethods() throws Exception { Method corsConfigurationSourceMethod = SecurityConfiguration.class.getDeclaredMethod("corsConfigurationSource"); corsConfigurationSourceMethod.setAccessible(true); CorsConfigurationSource corsConfigurationSource = (CorsConfigurationSource) corsConfigurationSourceMethod.invoke(securityConfiguration); - + MockHttpServletRequest request = new MockHttpServletRequest(); var corsConfiguration = corsConfigurationSource.getCorsConfiguration(request); @@ -84,7 +85,7 @@ void corsConfigurationShouldAllowExpectedHeaders() throws Exception { Method corsConfigurationSourceMethod = SecurityConfiguration.class.getDeclaredMethod("corsConfigurationSource"); corsConfigurationSourceMethod.setAccessible(true); CorsConfigurationSource corsConfigurationSource = (CorsConfigurationSource) corsConfigurationSourceMethod.invoke(securityConfiguration); - + MockHttpServletRequest request = new MockHttpServletRequest(); var corsConfiguration = corsConfigurationSource.getCorsConfiguration(request); @@ -107,7 +108,7 @@ void corsConfigurationShouldAllowCredentials() throws Exception { Method corsConfigurationSourceMethod = SecurityConfiguration.class.getDeclaredMethod("corsConfigurationSource"); corsConfigurationSourceMethod.setAccessible(true); CorsConfigurationSource corsConfigurationSource = (CorsConfigurationSource) corsConfigurationSourceMethod.invoke(securityConfiguration); - + MockHttpServletRequest request = new MockHttpServletRequest(); var corsConfiguration = corsConfigurationSource.getCorsConfiguration(request); @@ -125,7 +126,7 @@ void corsConfigurationShouldHaveCorrectMaxAge() throws Exception { Method corsConfigurationSourceMethod = SecurityConfiguration.class.getDeclaredMethod("corsConfigurationSource"); corsConfigurationSourceMethod.setAccessible(true); CorsConfigurationSource corsConfigurationSource = (CorsConfigurationSource) corsConfigurationSourceMethod.invoke(securityConfiguration); - + MockHttpServletRequest request = new MockHttpServletRequest(); var corsConfiguration = corsConfigurationSource.getCorsConfiguration(request); diff --git a/pointtils/src/test/resources/application.properties b/pointtils/src/test/resources/application.properties index 6b62c48f..b55d8b95 100644 --- a/pointtils/src/test/resources/application.properties +++ b/pointtils/src/test/resources/application.properties @@ -24,3 +24,7 @@ spring.flyway.enabled=false springdoc.api-docs.enabled=false springdoc.swagger-ui.enabled=false springdoc.swagger-ui.path=/swagger-ui.html + +# Clientes HTTP +client.ibge.state-url=http://exemplo.com.br/estados +client.ibge.city-url=http://exemplo.com.br/estados/{state}/municipios \ No newline at end of file