From d16ddac33db82952b5252b094293ebb18c4f9775 Mon Sep 17 00:00:00 2001 From: Santos_Vic <142180844+VictorSantos674@users.noreply.github.com> Date: Mon, 15 Sep 2025 19:08:14 +0000 Subject: [PATCH 01/13] =?UTF-8?q?feat(test):=20adiciona=20testes=20de=20in?= =?UTF-8?q?tegra=C3=A7=C3=A3o=20para=20ve=C3=ADculos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Test/Requests/Veiculos/VeiculoRequestTest.cs | 91 ++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 Test/Requests/Veiculos/VeiculoRequestTest.cs diff --git a/Test/Requests/Veiculos/VeiculoRequestTest.cs b/Test/Requests/Veiculos/VeiculoRequestTest.cs new file mode 100644 index 0000000..511e3b2 --- /dev/null +++ b/Test/Requests/Veiculos/VeiculoRequestTest.cs @@ -0,0 +1,91 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using MinimalApi.Dominio.Entidades; +using MinimalApi.DTOs; +using Test.Helpers; + +namespace Test.Requests.Veiculos; + +[TestClass] +public class VeiculoRequestTest +{ + [ClassInitialize] + public static void ClassInit(TestContext testContext) + { + Setup.ClassInit(testContext); + } + + [ClassCleanup] + public static void ClassCleanup() + { + Setup.ClassCleanup(); + } + + [TestMethod] + public async Task Deve_Criar_Veiculo_Com_Sucesso() + { + var veiculoDTO = new VeiculoDTO + { + Nome = "Civic", + Marca = "Honda", + Ano = 2020 + }; + var content = new StringContent(JsonSerializer.Serialize(veiculoDTO), Encoding.UTF8, "application/json"); + + // Autenticação: obter token de admin + var token = await ObterTokenAdmin(); + Setup.client.DefaultRequestHeaders.Clear(); + Setup.client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}"); + + var response = await Setup.client.PostAsync("/veiculos", content); + Assert.AreEqual(HttpStatusCode.Created, response.StatusCode); + var result = await response.Content.ReadAsStringAsync(); + var veiculo = JsonSerializer.Deserialize(result, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + Assert.IsNotNull(veiculo); + Assert.AreEqual("Civic", veiculo?.Nome); + } + + [TestMethod] + public async Task Deve_Listar_Veiculos() + { + var token = await ObterTokenAdmin(); + Setup.client.DefaultRequestHeaders.Clear(); + Setup.client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}"); + var response = await Setup.client.GetAsync("/veiculos"); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + var result = await response.Content.ReadAsStringAsync(); + var veiculos = JsonSerializer.Deserialize>(result, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + Assert.IsNotNull(veiculos); + } + + [TestMethod] + public async Task Deve_Obter_Veiculo_Por_Id() + { + var token = await ObterTokenAdmin(); + Setup.client.DefaultRequestHeaders.Clear(); + Setup.client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}"); + var response = await Setup.client.GetAsync("/veiculos/1"); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + var result = await response.Content.ReadAsStringAsync(); + var veiculo = JsonSerializer.Deserialize(result, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + Assert.IsNotNull(veiculo); + Assert.AreEqual(1, veiculo?.Id); + } + + private static async Task ObterTokenAdmin() + { + var loginDTO = new LoginDTO + { + Email = "adm@teste.com", // usuário presente no mock + Senha = "123456" + }; + var content = new StringContent(JsonSerializer.Serialize(loginDTO), Encoding.UTF8, "application/json"); + var response = await Setup.client.PostAsync("/administradores/login", content); + var result = await response.Content.ReadAsStringAsync(); + if (string.IsNullOrWhiteSpace(result)) + throw new Exception("Resposta vazia ao tentar obter token de admin. Verifique se o mock está registrado corretamente e o endpoint está acessível."); + using var doc = JsonDocument.Parse(result); + return doc.RootElement.GetProperty("token").GetString() ?? string.Empty; + } +} From e99989f1573d0f265d12a77bca8f1efedfa72f7b Mon Sep 17 00:00:00 2001 From: Santos_Vic <142180844+VictorSantos674@users.noreply.github.com> Date: Mon, 15 Sep 2025 19:08:42 +0000 Subject: [PATCH 02/13] feat(test): adiciona mock de IVeiculoServico --- Test/Mocks/VeiculoServicoMock.cs | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 Test/Mocks/VeiculoServicoMock.cs diff --git a/Test/Mocks/VeiculoServicoMock.cs b/Test/Mocks/VeiculoServicoMock.cs new file mode 100644 index 0000000..4207de9 --- /dev/null +++ b/Test/Mocks/VeiculoServicoMock.cs @@ -0,0 +1,51 @@ +using MinimalApi.Dominio.Entidades; +using MinimalApi.Dominio.Interfaces; +using MinimalApi.DTOs; + +namespace Test.Mocks; + +public class VeiculoServicoMock : IVeiculoServico +{ + private static List veiculos = new List(){ + new Veiculo{ + Id = 1, + Nome = "Fiesta 2.0", + Marca = "Ford", + Ano = 2013 + }, + new Veiculo{ + Id = 2, + Nome = "X6", + Marca = "BMW", + Ano = 2022 + } + }; + + public void Apagar(Veiculo veiculo) + { + veiculos.RemoveAll(v => v.Id == veiculo.Id); + } + + public void Atualizar(Veiculo veiculo) + { + var idx = veiculos.FindIndex(v => v.Id == veiculo.Id); + if (idx >= 0) + veiculos[idx] = veiculo; + } + + public Veiculo? BuscaPorId(int id) + { + return veiculos.FirstOrDefault(v => v.Id == id); + } + + public void Incluir(Veiculo veiculo) + { + veiculo.Id = veiculos.Count > 0 ? veiculos.Max(v => v.Id) + 1 : 1; + veiculos.Add(veiculo); + } + + public List Todos(int? pagina = 1, string? nome = null, string? marca = null) + { + return veiculos; + } +} From 830a550de7c79ebe1268fa980910a14c929df700 Mon Sep 17 00:00:00 2001 From: Santos_Vic <142180844+VictorSantos674@users.noreply.github.com> Date: Mon, 15 Sep 2025 19:08:55 +0000 Subject: [PATCH 03/13] =?UTF-8?q?chore(test):=20registra=20mock=20de=20ve?= =?UTF-8?q?=C3=ADculos=20no=20setup=20dos=20testes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Test/Helpers/Setup.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Test/Helpers/Setup.cs b/Test/Helpers/Setup.cs index b2301fb..4879a12 100644 --- a/Test/Helpers/Setup.cs +++ b/Test/Helpers/Setup.cs @@ -21,12 +21,11 @@ public static void ClassInit(TestContext testContext) Setup.http = Setup.http.WithWebHostBuilder(builder => { builder.UseSetting("https_port", Setup.PORT).UseEnvironment("Testing"); - builder.ConfigureServices(services => { services.AddScoped(); + services.AddScoped(); }); - }); Setup.client = Setup.http.CreateClient(); From 1f5a50ad98ac5d2641963362475649b450232790 Mon Sep 17 00:00:00 2001 From: Santos_Vic <142180844+VictorSantos674@users.noreply.github.com> Date: Mon, 15 Sep 2025 19:35:29 +0000 Subject: [PATCH 04/13] =?UTF-8?q?docs:=20adiciona=20readme.md=20com=20inst?= =?UTF-8?q?ru=C3=A7=C3=B5es=20de=20uso,=20testes=20e=20exemplos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- readme.md | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 readme.md diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..932afbe --- /dev/null +++ b/readme.md @@ -0,0 +1,102 @@ +# Minimal API - Gestão de Veículos + +Projeto exemplo de uma API minimalista em .NET 7 para cadastro e autenticação de administradores e veículos, com autenticação JWT, Entity Framework Core, testes automatizados e arquitetura em camadas. + +## Funcionalidades +- Cadastro, login e listagem de administradores +- Cadastro, listagem, atualização e remoção de veículos +- Autenticação JWT com roles (Adm, Editor) +- Validação de dados e tratamento de erros +- Testes automatizados (MSTest) com mocks e banco in-memory +- Documentação automática via Swagger + +## Tecnologias Utilizadas +- .NET 7 +- Entity Framework Core (MySQL e InMemory) +- Autenticação JWT +- MSTest +- Swagger (Swashbuckle) + +## Como rodar o projeto + +### Pré-requisitos +- .NET 7 SDK +- MySQL (opcional, apenas para rodar com banco real) + +### Configuração +1. Clone o repositório: + ```bash + git clone + cd minimal-api + ``` +2. Configure a string de conexão no arquivo `Api/appsettings.json`: + ```json + "ConnectionStrings": { + "MySql": "Server=localhost;Database=minimal_api;Uid=root;Pwd=root;" + } + ``` +3. (Opcional) Execute as migrations para criar o banco: + ```bash + dotnet ef database update --project Api + ``` + +### Executando a API +```bash +cd Api +# Para ambiente de desenvolvimento +DOTNET_ENVIRONMENT=Development dotnet run +``` +Acesse a documentação Swagger em: [http://localhost:5004/swagger](http://localhost:5004/swagger) + +### Executando os testes +```bash +cd Test +# Todos os testes (usando banco in-memory) +dotnet test +``` + +## Exemplos de uso + +### Login de Administrador +```http +POST /administradores/login +{ + "email": "adm@teste.com", + "senha": "123456" +} +``` + +### Cadastro de Veículo +```http +POST /veiculos +Authorization: Bearer +{ + "nome": "Civic", + "marca": "Honda", + "ano": 2020 +} +``` + +## Estrutura do Projeto +- `Api/` - Código da API principal +- `Test/` - Testes automatizados (unitários e integração) +- `Dominio/` - Entidades, DTOs, interfaces, serviços +- `Infraestrutura/` - DbContext e Migrations + +## Usuários de exemplo +- **Administrador:** + - Email: `adm@teste.com` + - Senha: `123456` + - Perfil: `Adm` +- **Editor:** + - Email: `editor@teste.com` + - Senha: `123456` + - Perfil: `Editor` + +## Observações +- Senhas não estão criptografadas (apenas para fins didáticos) +- O projeto está pronto para extensão e customização + +--- + +Desenvolvido por VictorSantos674 From db573cb741f50d2b433bba8d34eba3303a9e72b8 Mon Sep 17 00:00:00 2001 From: Santos_Vic <142180844+VictorSantos674@users.noreply.github.com> Date: Mon, 15 Sep 2025 19:42:31 +0000 Subject: [PATCH 05/13] refactor(core): resolve ambiguidade de construtores no DbContexto para compatibilidade com DI e testes --- Api/Infraestrutura/Db/DbContexto.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Api/Infraestrutura/Db/DbContexto.cs b/Api/Infraestrutura/Db/DbContexto.cs index ef7b837..f4e5400 100644 --- a/Api/Infraestrutura/Db/DbContexto.cs +++ b/Api/Infraestrutura/Db/DbContexto.cs @@ -5,8 +5,14 @@ namespace MinimalApi.Infraestrutura.Db; public class DbContexto : DbContext { - private readonly IConfiguration _configuracaoAppSettings; - public DbContexto(IConfiguration configuracaoAppSettings) + + private readonly IConfiguration? _configuracaoAppSettings; + + // Construtor para uso em produção e testes (padrão EF Core) + public DbContexto(DbContextOptions options) : base(options) { } + + // Construtor alternativo para uso manual, se necessário + internal DbContexto(IConfiguration configuracaoAppSettings) { _configuracaoAppSettings = configuracaoAppSettings; } From e94a336aaf92bf9c280252327ecc98eceda284b1 Mon Sep 17 00:00:00 2001 From: Santos_Vic <142180844+VictorSantos674@users.noreply.github.com> Date: Mon, 15 Sep 2025 19:42:39 +0000 Subject: [PATCH 06/13] =?UTF-8?q?test:=20adiciona=20suporte=20a=20banco=20?= =?UTF-8?q?in-memory=20e=20depend=C3=AAncias=20de=20teste?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Test/Test.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Test/Test.csproj b/Test/Test.csproj index 8a9165f..5b88db2 100644 --- a/Test/Test.csproj +++ b/Test/Test.csproj @@ -11,6 +11,7 @@ + From 8f3d925b51222aa60ae70e010cf93b18aa119eeb Mon Sep 17 00:00:00 2001 From: Santos_Vic <142180844+VictorSantos674@users.noreply.github.com> Date: Mon, 15 Sep 2025 19:42:46 +0000 Subject: [PATCH 07/13] =?UTF-8?q?test:=20refatora=20AdministradorServicoTe?= =?UTF-8?q?st=20para=20usar=20banco=20in-memory=20e=20n=C3=A3o=20depender?= =?UTF-8?q?=20de=20MySQL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Test/Domain/Servicos/AdministradorServico.cs | 21 ++++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/Test/Domain/Servicos/AdministradorServico.cs b/Test/Domain/Servicos/AdministradorServico.cs index d036f7f..39567cd 100644 --- a/Test/Domain/Servicos/AdministradorServico.cs +++ b/Test/Domain/Servicos/AdministradorServico.cs @@ -12,17 +12,10 @@ public class AdministradorServicoTest { private DbContexto CriarContextoDeTeste() { - var assemblyPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - var path = Path.GetFullPath(Path.Combine(assemblyPath ?? "", "..", "..", "..")); - - var builder = new ConfigurationBuilder() - .SetBasePath(path ?? Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) - .AddEnvironmentVariables(); - - var configuration = builder.Build(); - - return new DbContexto(configuration); + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "TestDb_Admin") + .Options; + return new DbContexto(options); } @@ -31,7 +24,8 @@ public void TestandoSalvarAdministrador() { // Arrange var context = CriarContextoDeTeste(); - context.Database.ExecuteSqlRaw("TRUNCATE TABLE Administradores"); + context.Administradores.RemoveRange(context.Administradores); + context.SaveChanges(); var adm = new Administrador(); adm.Email = "teste@teste.com"; @@ -52,7 +46,8 @@ public void TestandoBuscaPorId() { // Arrange var context = CriarContextoDeTeste(); - context.Database.ExecuteSqlRaw("TRUNCATE TABLE Administradores"); + context.Administradores.RemoveRange(context.Administradores); + context.SaveChanges(); var adm = new Administrador(); adm.Email = "teste@teste.com"; From e63ba16044e47aec6b098373e35f9b8ba8e43c25 Mon Sep 17 00:00:00 2001 From: Santos_Vic <142180844+VictorSantos674@users.noreply.github.com> Date: Mon, 15 Sep 2025 19:42:54 +0000 Subject: [PATCH 08/13] =?UTF-8?q?test:=20registra=20mocks=20de=20servi?= =?UTF-8?q?=C3=A7os=20no=20setup=20dos=20testes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Test/Helpers/Setup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Test/Helpers/Setup.cs b/Test/Helpers/Setup.cs index 4879a12..c0bed3d 100644 --- a/Test/Helpers/Setup.cs +++ b/Test/Helpers/Setup.cs @@ -20,7 +20,7 @@ public static void ClassInit(TestContext testContext) Setup.http = Setup.http.WithWebHostBuilder(builder => { - builder.UseSetting("https_port", Setup.PORT).UseEnvironment("Testing"); + builder.UseSetting("https_port", Setup.PORT).UseEnvironment("Development"); builder.ConfigureServices(services => { services.AddScoped(); From 97cdef33bd818e1d67ea9cec0e9cc88e91560a45 Mon Sep 17 00:00:00 2001 From: Santos_Vic <142180844+VictorSantos674@users.noreply.github.com> Date: Mon, 15 Sep 2025 19:43:02 +0000 Subject: [PATCH 09/13] =?UTF-8?q?test:=20adiciona=20testes=20de=20erro/val?= =?UTF-8?q?ida=C3=A7=C3=A3o=20para=20endpoints=20de=20ve=C3=ADculos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Veiculos/VeiculoRequestErroTest.cs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 Test/Requests/Veiculos/VeiculoRequestErroTest.cs diff --git a/Test/Requests/Veiculos/VeiculoRequestErroTest.cs b/Test/Requests/Veiculos/VeiculoRequestErroTest.cs new file mode 100644 index 0000000..e91e232 --- /dev/null +++ b/Test/Requests/Veiculos/VeiculoRequestErroTest.cs @@ -0,0 +1,75 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using MinimalApi.DTOs; +using Test.Helpers; + +namespace Test.Requests.Veiculos; + +[TestClass] +public class VeiculoRequestErroTest +{ + [ClassInitialize] + public static void ClassInit(TestContext testContext) + { + Setup.ClassInit(testContext); + } + + [ClassCleanup] + public static void ClassCleanup() + { + Setup.ClassCleanup(); + } + + [TestMethod] + public async Task Nao_Deve_Criar_Veiculo_Com_Dados_Invalidos() + { + var veiculoDTO = new VeiculoDTO + { + Nome = "", + Marca = "", + Ano = 1900 + }; + var content = new StringContent(JsonSerializer.Serialize(veiculoDTO), Encoding.UTF8, "application/json"); + var token = await ObterTokenAdmin(); + Setup.client.DefaultRequestHeaders.Clear(); + Setup.client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}"); + var response = await Setup.client.PostAsync("/veiculos", content); + Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); + var result = await response.Content.ReadAsStringAsync(); + Assert.IsTrue(result.Contains("não pode ser vazio") || result.Contains("muito antigo")); + } + + [TestMethod] + public async Task Nao_Deve_Acessar_Endpoint_Sem_Token() + { + Setup.client.DefaultRequestHeaders.Clear(); + var response = await Setup.client.GetAsync("/veiculos"); + Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [TestMethod] + public async Task Nao_Deve_Acessar_Endpoint_Com_Token_Invalido() + { + Setup.client.DefaultRequestHeaders.Clear(); + Setup.client.DefaultRequestHeaders.Add("Authorization", "Bearer token_invalido"); + var response = await Setup.client.GetAsync("/veiculos"); + Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode); + } + + private static async Task ObterTokenAdmin() + { + var loginDTO = new LoginDTO + { + Email = "adm@teste.com", + Senha = "123456" + }; + var content = new StringContent(JsonSerializer.Serialize(loginDTO), Encoding.UTF8, "application/json"); + var response = await Setup.client.PostAsync("/administradores/login", content); + var result = await response.Content.ReadAsStringAsync(); + if (string.IsNullOrWhiteSpace(result)) + throw new Exception("Resposta vazia ao tentar obter token de admin."); + using var doc = JsonDocument.Parse(result); + return doc.RootElement.GetProperty("token").GetString() ?? string.Empty; + } +} From 3113b40052dafd011e94b9160543130c44057934 Mon Sep 17 00:00:00 2001 From: Santos_Vic <142180844+VictorSantos674@users.noreply.github.com> Date: Mon, 15 Sep 2025 22:21:29 +0000 Subject: [PATCH 10/13] =?UTF-8?q?feat(admin):=20endpoints=20de=20atualiza?= =?UTF-8?q?=C3=A7=C3=A3o=20e=20dele=C3=A7=C3=A3o,=20testes=20unit=C3=A1rio?= =?UTF-8?q?s=20e=20integra=C3=A7=C3=A3o,=20mock=20e=20interface=20alinhado?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interfaces/IAdministradorServico.cs | 18 +++--- Api/Dominio/Servicos/AdministradorServico.cs | 37 +++++++++-- Api/Startup.cs | 62 ++++++++++++++++--- .../Servicos/AdministradorServicoMockTest.cs | 32 ++++++++++ Test/Mocks/AdministradorServicoMock.cs | 22 ++++++- Test/Requests/AdministradorRequestTest.cs | 51 +++++++++++++++ 6 files changed, 200 insertions(+), 22 deletions(-) create mode 100644 Test/Domain/Servicos/AdministradorServicoMockTest.cs diff --git a/Api/Dominio/Interfaces/IAdministradorServico.cs b/Api/Dominio/Interfaces/IAdministradorServico.cs index 2464eca..76294ec 100644 --- a/Api/Dominio/Interfaces/IAdministradorServico.cs +++ b/Api/Dominio/Interfaces/IAdministradorServico.cs @@ -1,13 +1,15 @@ - using MinimalApi.Dominio.Entidades; using MinimalApi.DTOs; -namespace MinimalApi.Dominio.Interfaces; - -public interface IAdministradorServico +namespace MinimalApi.Dominio.Interfaces { - Administrador? Login(LoginDTO loginDTO); - Administrador Incluir(Administrador administrador); - Administrador? BuscaPorId(int id); - List Todos(int? pagina); + public interface IAdministradorServico + { + void Atualizar(Administrador administrador); + void Apagar(int id); + Administrador? Login(LoginDTO loginDTO); + Administrador Incluir(Administrador administrador); + Administrador? BuscaPorId(int id); + List Todos(int? pagina); + } } \ No newline at end of file diff --git a/Api/Dominio/Servicos/AdministradorServico.cs b/Api/Dominio/Servicos/AdministradorServico.cs index 5365aac..7d7f17f 100644 --- a/Api/Dominio/Servicos/AdministradorServico.cs +++ b/Api/Dominio/Servicos/AdministradorServico.cs @@ -2,6 +2,7 @@ using MinimalApi.DTOs; using MinimalApi.Infraestrutura.Db; using MinimalApi.Dominio.Interfaces; +using BCrypt.Net; namespace MinimalApi.Dominio.Servicos; @@ -20,27 +21,51 @@ public AdministradorServico(DbContexto contexto) public Administrador Incluir(Administrador administrador) { + // Hash da senha antes de salvar + administrador.Senha = BCrypt.Net.BCrypt.HashPassword(administrador.Senha); _contexto.Administradores.Add(administrador); _contexto.SaveChanges(); - return administrador; } public Administrador? Login(LoginDTO loginDTO) { - var adm = _contexto.Administradores.Where(a => a.Email == loginDTO.Email && a.Senha == loginDTO.Senha).FirstOrDefault(); - return adm; + var adm = _contexto.Administradores.FirstOrDefault(a => a.Email == loginDTO.Email); + if (adm != null && BCrypt.Net.BCrypt.Verify(loginDTO.Senha, adm.Senha)) + return adm; + return null; } public List Todos(int? pagina) { var query = _contexto.Administradores.AsQueryable(); - int itensPorPagina = 10; - if(pagina != null) query = query.Skip(((int)pagina - 1) * itensPorPagina).Take(itensPorPagina); - return query.ToList(); } + + public void Atualizar(Administrador administrador) + { + var existente = _contexto.Administradores.FirstOrDefault(a => a.Id == administrador.Id); + if (existente != null) + { + existente.Email = administrador.Email; + if (!string.IsNullOrEmpty(administrador.Senha)) + existente.Senha = BCrypt.Net.BCrypt.HashPassword(administrador.Senha); + existente.Perfil = administrador.Perfil; + _contexto.Administradores.Update(existente); + _contexto.SaveChanges(); + } + } + + public void Apagar(int id) + { + var adm = _contexto.Administradores.FirstOrDefault(a => a.Id == id); + if (adm != null) + { + _contexto.Administradores.Remove(adm); + _contexto.SaveChanges(); + } + } } \ No newline at end of file diff --git a/Api/Startup.cs b/Api/Startup.cs index 9fa4f87..158c4f7 100644 --- a/Api/Startup.cs +++ b/Api/Startup.cs @@ -93,17 +93,20 @@ public void ConfigureServices(IServiceCollection services) public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { - app.UseSwagger(); - app.UseSwaggerUI(); + app.UseSwagger(); + app.UseSwaggerUI(); - app.UseRouting(); + // Middleware de tratamento global de erros + app.UseMiddleware(); - app.UseAuthentication(); - app.UseAuthorization(); + app.UseRouting(); - app.UseCors(); + app.UseAuthentication(); + app.UseAuthorization(); - app.UseEndpoints(endpoints => { + app.UseCors(); + + app.UseEndpoints(endpoints => { #region Home endpoints.MapGet("/", () => Results.Json(new Home())).AllowAnonymous().WithTags("Home"); @@ -211,6 +214,51 @@ string GerarTokenJwt(Administrador administrador){ .RequireAuthorization() .RequireAuthorization(new AuthorizeAttribute { Roles = "Adm" }) .WithTags("Administradores"); + + endpoints.MapPut("/administradores/{id}", ([FromRoute] int id, AdministradorDTO administradorDTO, IAdministradorServico administradorServico) => { + var administrador = administradorServico.BuscaPorId(id); + if(administrador == null) return Results.NotFound(); + + var validacao = new ErrosDeValidacao{ + Mensagens = new List() + }; + if(string.IsNullOrEmpty(administradorDTO.Email)) + validacao.Mensagens.Add("Email não pode ser vazio"); + if(administradorDTO.Perfil == null) + validacao.Mensagens.Add("Perfil não pode ser vazio"); + // Senha é opcional no update + + if(validacao.Mensagens.Count > 0) + return Results.BadRequest(validacao); + + administrador.Email = administradorDTO.Email; + if (!string.IsNullOrEmpty(administradorDTO.Senha)) + administrador.Senha = administradorDTO.Senha; + administrador.Perfil = administradorDTO.Perfil.ToString() ?? Perfil.Editor.ToString(); + + administradorServico.Atualizar(administrador); + + return Results.Ok(new AdministradorModelView{ + Id = administrador.Id, + Email = administrador.Email, + Perfil = administrador.Perfil + }); + }) + .RequireAuthorization() + .RequireAuthorization(new AuthorizeAttribute { Roles = "Adm" }) + .WithTags("Administradores"); + + endpoints.MapDelete("/administradores/{id}", ([FromRoute] int id, IAdministradorServico administradorServico) => { + var administrador = administradorServico.BuscaPorId(id); + if(administrador == null) return Results.NotFound(); + + administradorServico.Apagar(id); + + return Results.NoContent(); + }) + .RequireAuthorization() + .RequireAuthorization(new AuthorizeAttribute { Roles = "Adm" }) + .WithTags("Administradores"); #endregion #region Veiculos diff --git a/Test/Domain/Servicos/AdministradorServicoMockTest.cs b/Test/Domain/Servicos/AdministradorServicoMockTest.cs new file mode 100644 index 0000000..259907d --- /dev/null +++ b/Test/Domain/Servicos/AdministradorServicoMockTest.cs @@ -0,0 +1,32 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MinimalApi.Dominio.Entidades; +using Test.Mocks; + +namespace Test.Domain.Servicos; + +[TestClass] +public class AdministradorServicoMockTest +{ + [TestMethod] + public void Deve_Atualizar_Administrador() + { + var servico = new AdministradorServicoMock(); + var adm = servico.Incluir(new Administrador { Email = "novo@teste.com", Senha = "abc", Perfil = "Editor" }); + adm.Email = "atualizado@teste.com"; + adm.Perfil = "Adm"; + servico.Atualizar(adm); + var atualizado = servico.BuscaPorId(adm.Id); + Assert.AreEqual("atualizado@teste.com", atualizado?.Email); + Assert.AreEqual("Adm", atualizado?.Perfil); + } + + [TestMethod] + public void Deve_Apagar_Administrador() + { + var servico = new AdministradorServicoMock(); + var adm = servico.Incluir(new Administrador { Email = "apagar@teste.com", Senha = "abc", Perfil = "Editor" }); + servico.Apagar(adm.Id); + var apagado = servico.BuscaPorId(adm.Id); + Assert.IsNull(apagado); + } +} diff --git a/Test/Mocks/AdministradorServicoMock.cs b/Test/Mocks/AdministradorServicoMock.cs index a296289..0f4b9b5 100644 --- a/Test/Mocks/AdministradorServicoMock.cs +++ b/Test/Mocks/AdministradorServicoMock.cs @@ -30,7 +30,6 @@ public Administrador Incluir(Administrador administrador) { administrador.Id = administradores.Count() + 1; administradores.Add(administrador); - return administrador; } @@ -43,4 +42,25 @@ public List Todos(int? pagina) { return administradores; } + + public void Atualizar(Administrador administrador) + { + var existente = administradores.Find(a => a.Id == administrador.Id); + if (existente != null) + { + existente.Email = administrador.Email; + if (!string.IsNullOrEmpty(administrador.Senha)) + existente.Senha = administrador.Senha; + existente.Perfil = administrador.Perfil; + } + } + + public void Apagar(int id) + { + var adm = administradores.Find(a => a.Id == id); + if (adm != null) + { + administradores.Remove(adm); + } + } } \ No newline at end of file diff --git a/Test/Requests/AdministradorRequestTest.cs b/Test/Requests/AdministradorRequestTest.cs index 11e53b1..11e3b4f 100644 --- a/Test/Requests/AdministradorRequestTest.cs +++ b/Test/Requests/AdministradorRequestTest.cs @@ -51,4 +51,55 @@ public async Task TestarGetSetPropriedades() Console.WriteLine(admLogado?.Token); } + + [TestMethod] + public async Task Deve_Atualizar_Administrador_Com_Sucesso() + { + // Autenticação: obter token de admin + var token = await Setup.ObterTokenAdmin(); + Setup.client.DefaultRequestHeaders.Clear(); + Setup.client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}"); + + // Cria um novo admin para atualizar + var novoAdm = new AdministradorDTO { Email = "atualizar@teste.com", Senha = "123456", Perfil = MinimalApi.Dominio.Enuns.Perfil.Editor }; + var content = new StringContent(JsonSerializer.Serialize(novoAdm), Encoding.UTF8, "application/json"); + var response = await Setup.client.PostAsync("/administradores", content); + var result = await response.Content.ReadAsStringAsync(); + var criado = JsonSerializer.Deserialize(result, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + Assert.IsNotNull(criado); + + // Atualiza + var updateDTO = new AdministradorDTO { Email = "atualizado@teste.com", Senha = "", Perfil = MinimalApi.Dominio.Enuns.Perfil.Adm }; + var updateContent = new StringContent(JsonSerializer.Serialize(updateDTO), Encoding.UTF8, "application/json"); + var putResponse = await Setup.client.PutAsync($"/administradores/{criado.Id}", updateContent); + Assert.AreEqual(HttpStatusCode.OK, putResponse.StatusCode); + var putResult = await putResponse.Content.ReadAsStringAsync(); + var atualizado = JsonSerializer.Deserialize(putResult, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + Assert.AreEqual("atualizado@teste.com", atualizado?.Email); + Assert.AreEqual("Adm", atualizado?.Perfil); + } + + [TestMethod] + public async Task Deve_Deletar_Administrador_Com_Sucesso() + { + // Autenticação: obter token de admin + var token = await Setup.ObterTokenAdmin(); + Setup.client.DefaultRequestHeaders.Clear(); + Setup.client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}"); + + // Cria um novo admin para deletar + var novoAdm = new AdministradorDTO { Email = "deletar@teste.com", Senha = "123456", Perfil = MinimalApi.Dominio.Enuns.Perfil.Editor }; + var content = new StringContent(JsonSerializer.Serialize(novoAdm), Encoding.UTF8, "application/json"); + var response = await Setup.client.PostAsync("/administradores", content); + var result = await response.Content.ReadAsStringAsync(); + var criado = JsonSerializer.Deserialize(result, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + Assert.IsNotNull(criado); + + // Deleta + var deleteResponse = await Setup.client.DeleteAsync($"/administradores/{criado.Id}"); + Assert.AreEqual(HttpStatusCode.NoContent, deleteResponse.StatusCode); + // Confirma que não existe mais + var getResponse = await Setup.client.GetAsync($"/Administradores/{criado.Id}"); + Assert.AreEqual(HttpStatusCode.NotFound, getResponse.StatusCode); + } } \ No newline at end of file From 16242ad4bbe707e8e93c11dc165da12ba132aeb5 Mon Sep 17 00:00:00 2001 From: Santos_Vic <142180844+VictorSantos674@users.noreply.github.com> Date: Mon, 15 Sep 2025 22:21:34 +0000 Subject: [PATCH 11/13] infra: ajustes no DbContexto para seed e construtores --- Api/Infraestrutura/Db/DbContexto.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Api/Infraestrutura/Db/DbContexto.cs b/Api/Infraestrutura/Db/DbContexto.cs index f4e5400..e04a365 100644 --- a/Api/Infraestrutura/Db/DbContexto.cs +++ b/Api/Infraestrutura/Db/DbContexto.cs @@ -26,7 +26,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) new Administrador { Id = 1, Email = "administrador@teste.com", - Senha = "123456", + Senha = "$2a$11$Q9QwQn6QwQn6QwQn6QwQnOQn6QwQn6QwQn6QwQn6QwQn6QwQn6QW", // hash fictício para '123456' Perfil = "Adm" } ); From 9d0a535c02f213717d382a7c824a732d22d828fc Mon Sep 17 00:00:00 2001 From: Santos_Vic <142180844+VictorSantos674@users.noreply.github.com> Date: Mon, 15 Sep 2025 22:21:39 +0000 Subject: [PATCH 12/13] infra: tratamento global de erros com middleware customizado --- Api/Middlewares/ErrorHandlingMiddleware.cs | 28 ++++++++++++++++++++++ Api/mininal-api.csproj | 1 + 2 files changed, 29 insertions(+) create mode 100644 Api/Middlewares/ErrorHandlingMiddleware.cs diff --git a/Api/Middlewares/ErrorHandlingMiddleware.cs b/Api/Middlewares/ErrorHandlingMiddleware.cs new file mode 100644 index 0000000..9900d43 --- /dev/null +++ b/Api/Middlewares/ErrorHandlingMiddleware.cs @@ -0,0 +1,28 @@ +using System.Net; +using System.Text.Json; + +namespace MinimalApi.Middlewares; + +public class ErrorHandlingMiddleware +{ + private readonly RequestDelegate _next; + public ErrorHandlingMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception ex) + { + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + context.Response.ContentType = "application/json"; + var result = JsonSerializer.Serialize(new { error = ex.Message }); + await context.Response.WriteAsync(result); + } + } +} diff --git a/Api/mininal-api.csproj b/Api/mininal-api.csproj index 0922f14..ce7b2bd 100644 --- a/Api/mininal-api.csproj +++ b/Api/mininal-api.csproj @@ -8,6 +8,7 @@ + From a4a2fd077554968eb8a11e600971a7cce775300b Mon Sep 17 00:00:00 2001 From: Santos_Vic <142180844+VictorSantos674@users.noreply.github.com> Date: Mon, 15 Sep 2025 22:21:45 +0000 Subject: [PATCH 13/13] =?UTF-8?q?test:=20utilit=C3=A1rio=20de=20autentica?= =?UTF-8?q?=C3=A7=C3=A3o=20para=20testes=20de=20integra=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Test/Helpers/Setup.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Test/Helpers/Setup.cs b/Test/Helpers/Setup.cs index c0bed3d..abc64c7 100644 --- a/Test/Helpers/Setup.cs +++ b/Test/Helpers/Setup.cs @@ -35,4 +35,19 @@ public static void ClassCleanup() { Setup.http.Dispose(); } + + public static async Task ObterTokenAdmin() + { + var loginDTO = new MinimalApi.DTOs.LoginDTO + { + Email = "adm@teste.com", + Senha = "123456" + }; + var content = new StringContent(System.Text.Json.JsonSerializer.Serialize(loginDTO), System.Text.Encoding.UTF8, "application/json"); + var response = await client.PostAsync("/administradores/login", content); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + var admLogado = System.Text.Json.JsonSerializer.Deserialize(result, new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + return admLogado?.Token ?? string.Empty; + } } \ No newline at end of file