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/Infraestrutura/Db/DbContexto.cs b/Api/Infraestrutura/Db/DbContexto.cs index ef7b837..e04a365 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; } @@ -20,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" } ); 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/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/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 @@ + 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"; 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/Helpers/Setup.cs b/Test/Helpers/Setup.cs index b2301fb..abc64c7 100644 --- a/Test/Helpers/Setup.cs +++ b/Test/Helpers/Setup.cs @@ -20,13 +20,12 @@ 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(); + services.AddScoped(); }); - }); Setup.client = Setup.http.CreateClient(); @@ -36,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 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/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; + } +} 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 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; + } +} 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; + } +} 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 @@ + 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