diff --git a/src/test/java/br/senac/xanxere/seguranca/AutenticacaoTest.java b/src/test/java/br/senac/xanxere/seguranca/AutenticacaoTest.java new file mode 100644 index 0000000..40f846d --- /dev/null +++ b/src/test/java/br/senac/xanxere/seguranca/AutenticacaoTest.java @@ -0,0 +1,69 @@ +package br.senac.xanxere.seguranca; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +/** + * AUTENTICAÇÃO: quem é você? + * + * Toda falha aqui deve resultar em 401 Unauthorized, nunca em 403. + */ +@SpringBootTest +@AutoConfigureMockMvc +class AutenticacaoTest { + + @Autowired + private MockMvc mvc; + + @Test + @DisplayName("rota pública responde 200 sem nenhuma credencial") + void rotaPublicaNaoExigeLogin() throws Exception { + mvc.perform(get("/api/publico")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.mensagem").exists()); + } + + @Test + @DisplayName("rota protegida sem credenciais responde 401") + void semCredenciaisRetorna401() throws Exception { + mvc.perform(get("/api/perfil")) + .andExpect(status().isUnauthorized()) + // HttpStatusEntryPoint não manda o header que faz o navegador abrir popup de login + .andExpect(header().doesNotExist("WWW-Authenticate")); + } + + @Test + @DisplayName("senha errada responde 401") + void senhaErradaRetorna401() throws Exception { + mvc.perform(get("/api/perfil").with(httpBasic("aluno", "senha-errada"))) + .andExpect(status().isUnauthorized()); + } + + @Test + @DisplayName("usuário inexistente responde 401 (mesma resposta da senha errada, para não vazar quem existe)") + void usuarioInexistenteRetorna401() throws Exception { + mvc.perform(get("/api/perfil").with(httpBasic("ninguem", "qualquer"))) + .andExpect(status().isUnauthorized()); + } + + @Test + @DisplayName("credenciais corretas autenticam e o perfil traz login e papéis com prefixo ROLE_") + void credenciaisCorretasAutenticam() throws Exception { + mvc.perform(get("/api/perfil").with(httpBasic("admin", "admin123"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.login").value("admin")) + .andExpect(jsonPath("$.papeis").isArray()) + .andExpect(jsonPath("$.papeis[?(@ == 'ROLE_ADMIN')]").exists()) + .andExpect(jsonPath("$.papeis[?(@ == 'ROLE_PROFESSOR')]").exists()); + } +} diff --git a/src/test/java/br/senac/xanxere/seguranca/AutorizacaoPorMetodoTest.java b/src/test/java/br/senac/xanxere/seguranca/AutorizacaoPorMetodoTest.java new file mode 100644 index 0000000..cb86d28 --- /dev/null +++ b/src/test/java/br/senac/xanxere/seguranca/AutorizacaoPorMetodoTest.java @@ -0,0 +1,62 @@ +package br.senac.xanxere.seguranca; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +/** + * AUTORIZAÇÃO por MÉTODO: a regra @PreAuthorize de NotasController. + * + * "hasAnyRole('PROFESSOR', 'ADMIN') or #aluno == authentication.name" + */ +@SpringBootTest +@AutoConfigureMockMvc +class AutorizacaoPorMetodoTest { + + @Autowired + private MockMvc mvc; + + @Test + @DisplayName("aluno vê as próprias notas") + void alunoVeAsPropriasNotas() throws Exception { + mvc.perform(get("/api/notas/aluno").with(httpBasic("aluno", "aluno123"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.Programação").value(8.5)); + } + + @Test + @DisplayName("aluno não vê as notas de outro aluno (403)") + void alunoNaoVeNotasDeOutro() throws Exception { + mvc.perform(get("/api/notas/outro").with(httpBasic("aluno", "aluno123"))) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("professor vê as notas de qualquer aluno") + void professorVeNotasDeQualquerAluno() throws Exception { + mvc.perform(get("/api/notas/aluno").with(httpBasic("professor", "prof123"))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("admin vê as notas de qualquer aluno") + void adminVeNotasDeQualquerAluno() throws Exception { + mvc.perform(get("/api/notas/aluno").with(httpBasic("admin", "admin123"))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("sem login continua sendo 401, mesmo com regra de método") + void semLoginRetorna401() throws Exception { + mvc.perform(get("/api/notas/aluno")) + .andExpect(status().isUnauthorized()); + } +} diff --git a/src/test/java/br/senac/xanxere/seguranca/AutorizacaoPorUrlTest.java b/src/test/java/br/senac/xanxere/seguranca/AutorizacaoPorUrlTest.java new file mode 100644 index 0000000..c269511 --- /dev/null +++ b/src/test/java/br/senac/xanxere/seguranca/AutorizacaoPorUrlTest.java @@ -0,0 +1,87 @@ +package br.senac.xanxere.seguranca; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +/** + * AUTORIZAÇÃO por URL: o que cada papel pode acessar? + * + * Aqui o usuário SEMPRE está autenticado; a única pergunta é 200 ou 403. + */ +@SpringBootTest +@AutoConfigureMockMvc +class AutorizacaoPorUrlTest { + + @Autowired + private MockMvc mvc; + + /** + * Matriz completa: (usuário, endpoint) -> status esperado. Espelha a tabela do README. + */ + static Stream matriz() { + return Stream.of( + Arguments.of("aluno", "aluno123", "/api/aluno", HttpStatus.OK), + Arguments.of("aluno", "aluno123", "/api/professor", HttpStatus.FORBIDDEN), + Arguments.of("aluno", "aluno123", "/api/admin", HttpStatus.FORBIDDEN), + + Arguments.of("professor", "prof123", "/api/aluno", HttpStatus.OK), + Arguments.of("professor", "prof123", "/api/professor", HttpStatus.OK), + Arguments.of("professor", "prof123", "/api/admin", HttpStatus.FORBIDDEN), + + Arguments.of("admin", "admin123", "/api/aluno", HttpStatus.OK), + Arguments.of("admin", "admin123", "/api/professor", HttpStatus.OK), + Arguments.of("admin", "admin123", "/api/admin", HttpStatus.OK) + ); + } + + @ParameterizedTest(name = "{0} em {2} -> {3}") + @MethodSource("matriz") + void papelContraEndpoint(String login, String senha, String url, HttpStatus esperado) throws Exception { + mvc.perform(get(url).with(httpBasic(login, senha))) + .andExpect(status().is(esperado.value())); + } + + /** + * Alternativa ao httpBasic: @WithMockUser cria um usuário fictício já autenticado, + * sem passar pelo UserDetailsService. Útil para testar SÓ a autorização. + */ + @Nested + @DisplayName("com @WithMockUser") + class ComUsuarioSimulado { + + @Test + @WithMockUser(roles = "ALUNO") + void alunoSimuladoNaoEntraNaAreaAdmin() throws Exception { + mvc.perform(get("/api/admin")).andExpect(status().isForbidden()); + } + + @Test + @WithMockUser(roles = "ADMIN") + void adminSimuladoEntraNaAreaAdmin() throws Exception { + mvc.perform(get("/api/admin")).andExpect(status().isOk()); + } + + @Test + @DisplayName("authorities sem o prefixo ROLE_ não valem como papel") + @WithMockUser(authorities = "ADMIN") + void authoritySemPrefixoNaoEhPapel() throws Exception { + mvc.perform(get("/api/admin")).andExpect(status().isForbidden()); + } + } +} diff --git a/src/test/java/br/senac/xanxere/seguranca/usuario/UsuarioDetailsServiceTest.java b/src/test/java/br/senac/xanxere/seguranca/usuario/UsuarioDetailsServiceTest.java new file mode 100644 index 0000000..7dc62ed --- /dev/null +++ b/src/test/java/br/senac/xanxere/seguranca/usuario/UsuarioDetailsServiceTest.java @@ -0,0 +1,58 @@ +package br.senac.xanxere.seguranca.usuario; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * Teste unitário puro: não sobe o contexto do Spring, só instancia as classes. + * Verifica a ponte entre o nosso Usuario e o UserDetails que o Spring Security consome. + */ +class UsuarioDetailsServiceTest { + + private PasswordEncoder encoder; + private UsuarioDetailsService service; + + @BeforeEach + void setUp() { + encoder = new BCryptPasswordEncoder(); + service = new UsuarioDetailsService(new UsuarioRepository(encoder)); + } + + @Test + @DisplayName("carrega o usuário com os papéis convertidos em authorities ROLE_*") + void carregaUsuarioComPapeis() { + UserDetails admin = service.loadUserByUsername("admin"); + + assertThat(admin.getUsername()).isEqualTo("admin"); + assertThat(admin.getAuthorities()) + .extracting(GrantedAuthority::getAuthority) + .containsExactlyInAnyOrder("ROLE_ADMIN", "ROLE_PROFESSOR"); + } + + @Test + @DisplayName("a senha guardada é um hash BCrypt que confere com a senha original") + void senhaGuardadaEhHashBcrypt() { + UserDetails aluno = service.loadUserByUsername("aluno"); + + assertThat(aluno.getPassword()).startsWith("$2").isNotEqualTo("aluno123"); + assertThat(encoder.matches("aluno123", aluno.getPassword())).isTrue(); + assertThat(encoder.matches("outra", aluno.getPassword())).isFalse(); + } + + @Test + @DisplayName("usuário desconhecido lança UsernameNotFoundException") + void usuarioDesconhecidoLancaExcecao() { + assertThatThrownBy(() -> service.loadUserByUsername("ninguem")) + .isInstanceOf(UsernameNotFoundException.class) + .hasMessageContaining("ninguem"); + } +}