Com a regra de negócio pronta (Módulo 02), este módulo expõe tudo como endpoints REST: registrar movimentação, consultar histórico, pesquisar produtos (RF09) e listar os que estão com estoque baixo (RF11).
MovimentacaoEstoqueControllerAção: o endpoint de registrar (POST) exige ADMIN ou ESTOQUISTA. Os de leitura (GET) ficam abertos a qualquer usuário autenticado — mesmo padrão de _01 (leitura ampla, escrita restrita).
controller/MovimentacaoEstoqueController.java@RestController
@RequestMapping("/api/movimentacoes")
@Tag(name = "Movimentações de Estoque", description = "Endpoints para registrar entradas/saídas e consultar o histórico")
@SecurityRequirement(name = "bearerAuth")
public class MovimentacaoEstoqueController {
private final MovimentacaoEstoqueService service;
public MovimentacaoEstoqueController(MovimentacaoEstoqueService service) {
this.service = service;
}
@GetMapping
public ResponseEntity<List<MovimentacaoEstoqueDTO>> findAll() {
return ResponseEntity.ok(service.findAll());
}
@GetMapping("/produto/{produtoId}")
public ResponseEntity<List<MovimentacaoEstoqueDTO>> findByProdutoId(@PathVariable Integer produtoId) {
return ResponseEntity.ok(service.findByProdutoId(produtoId));
}
@PostMapping
@PreAuthorize("hasAnyRole('ADMIN', 'ESTOQUISTA')")
public ResponseEntity<MovimentacaoEstoqueDTO> registrar(@Valid @RequestBody MovimentacaoEstoqueFormDTO formDTO) {
MovimentacaoEstoqueDTO dto = service.registrar(formDTO);
return ResponseEntity.status(HttpStatus.CREATED).body(dto);
}
}
💡
hasAnyRole('ADMIN', 'ESTOQUISTA')em vez de dois@PreAuthorizeseparados? O Spring Security avalia a expressão SpEL uma vez só — não existe “empilhar”@PreAuthorizeno mesmo método. Quando mais de um papel deveria ter acesso ao mesmo endpoint, a expressão é semprehasAnyRole(...)com todos eles.
ProdutoControllerAção: dois GETs novos, ambos leitura (sem @PreAuthorize extra — herdam a regra “qualquer autenticado” do restante do controller).
controller/ProdutoController.java (endpoints novos)@GetMapping("/busca")
public ResponseEntity<List<ProdutoDTO>> buscarPorNome(@RequestParam String nome) {
return ResponseEntity.ok(service.buscarPorNome(nome));
}
@GetMapping("/estoque-baixo")
public ResponseEntity<List<ProdutoDTO>> findComEstoqueBaixo() {
return ResponseEntity.ok(service.findComEstoqueBaixo());
}
⚠️ Ordem de rotas importa?
/api/produtos/buscae/api/produtos/{id}parecem poder colidir (buscacairia em{id}?), mas o Spring MVC resolve isso corretamente: um segmento estático (/busca,/estoque-baixo) sempre vence um@PathVariable(/{id}) na hora de escolher o handler, independente da ordem de declaração no arquivo. Ainda assim, é boa prática declarar as rotas estáticas antes das variáveis no código-fonte, por legibilidade.
ProdutoServiceImpl ganhou buscarPorNome/findComEstoqueBaixo, reaproveitando o mesmo helper privado de busca em lote (mapToProdutoDTOs) que findAll() já usava — extraído numa função só para não duplicar a lógica de N+1 três vezes.
@WebMvcTestConceito-Chave: testar @PreAuthorize isoladamente (sem subir o Spring Boot inteiro) exige atenção: @WebMvcTest não carrega automaticamente a @Configuration da aplicação (SecurityConfig) — ele aplica a configuração de segurança padrão do Spring Boot (CSRF ligado, sem @EnableMethodSecurity), o que faz o @PreAuthorize simplesmente não ser avaliado.
MovimentacaoEstoqueControllerTest.java (configuração do slice)@WebMvcTest(MovimentacaoEstoqueController.class)
@Import(SecurityConfig.class) // traz @EnableMethodSecurity + o filtro real (CSRF desligado)
@AutoConfigureMockMvc(addFilters = false) // @PreAuthorize é AOP, não depende de Filters
class MovimentacaoEstoqueControllerTest {
@MockBean private MovimentacaoEstoqueService movimentacaoEstoqueService;
@MockBean private JwtAuthenticationFilter jwtAuthenticationFilter; // dependência do SecurityConfig
@MockBean private UserDetailsService userDetailsService; // idem
@Test
@WithMockUser(roles = "ESTOQUISTA")
void registrar_shouldReturn201Created_whenUserIsEstoquista() throws Exception {
// ... given(...).willReturn(...) ...
mockMvc.perform(post("/api/movimentacoes")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(form)))
.andExpect(status().isCreated());
}
@Test
@WithMockUser(roles = "USER")
void registrar_shouldReturn403Forbidden_whenUserIsCommonUser() throws Exception {
mockMvc.perform(post("/api/movimentacoes")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(form)))
.andExpect(status().isForbidden());
}
}
Por que cada peça é necessária:
@Import(SecurityConfig.class) — sem isso, @EnableMethodSecurity(prePostEnabled = true) (declarado em SecurityConfig) nunca é processado no contexto do slice, e o AOP que intercepta @PreAuthorize não existe. O primeiro sintoma foi surpreendente: os testes passavam com status 200 tanto para ESTOQUISTA quanto para USER — a autorização estava sendo silenciosamente ignorada, não bloqueada.@AutoConfigureMockMvc(addFilters = false) — @PreAuthorize é um interceptor em torno do bean do controller (Spring AOP), não depende da cadeia de Servlet Filters. Desligar os filtros evita dois problemas de uma vez: (a) o CsrfFilter padrão bloqueando POST/DELETE sem token, e (b) o bean mockado de JwtAuthenticationFilter (@MockBean, sem nenhum doFilter() stubado) sendo registrado como um Filter real e silenciosamente engolindo a requisição (um mock sem stub não chama filterChain.doFilter(), então a cadeia simplesmente para ali).@MockBean UserDetailsService — SecurityConfig.authenticationProvider() precisa desse bean para ser construído; sem ele, o contexto do teste falha ao subir (mesmo com os filtros desligados, o @Bean securityFilterChain(...) ainda precisa ser instanciado).Isso revelou também que GlobalExceptionHandler não tratava AccessDeniedException (ver Módulo 02, Aula 2.1) — sem o ExceptionTranslationFilter da cadeia real presente no teste, a exceção do @PreAuthorize negado caía direto no handler genérico e virava 500 em vez de 403. O handler dedicado corrige isso tanto no teste quanto como rede de segurança em produção.
A API está completa: movimentações com autorização por papel, busca e alerta de estoque mínimo. No próximo e último módulo, fechamos com os relatórios (RF12) e as telas novas da SPA.