From e47e08b9f032235f5f151cc03c63a6dc89d55d04 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:29:41 -0500 Subject: [PATCH] feat(infra): add delete endpoint for transactions with corresponding integration tests --- .../Transactions/DeleteById/Endpoint.cs | 34 +++++ .../Transactions/TransactionsExtensions.cs | 2 + .../Transactions/DeleteByIdTests.cs | 120 ++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 src/FiscalOS.API/Transactions/DeleteById/Endpoint.cs create mode 100644 tests/FiscalOS.API.Tests/Integration/Transactions/DeleteByIdTests.cs diff --git a/src/FiscalOS.API/Transactions/DeleteById/Endpoint.cs b/src/FiscalOS.API/Transactions/DeleteById/Endpoint.cs new file mode 100644 index 0000000..b3e2eec --- /dev/null +++ b/src/FiscalOS.API/Transactions/DeleteById/Endpoint.cs @@ -0,0 +1,34 @@ +namespace FiscalOS.API.Transactions.DeleteById; + +internal static class Endpoint +{ + private const string Route = "/{id}"; + + public static RouteHandlerBuilder MapDeleteEndpoint(this RouteGroupBuilder groupBuilder) + { + return groupBuilder.MapDelete(Route, HandleAsync); + } + + private static async Task HandleAsync( + HttpContext httpContext, + [FromRoute] Guid id, + [FromServices] AppDbContext appDbContext, + CancellationToken ct + ) + { + var userId = httpContext.GetUserId(); + + var existingTransaction = await appDbContext.Transactions + .SingleOrDefaultAsync(t => t.UserId == userId && t.Id == id, ct); + + if (existingTransaction is null) + { + return Results.NotFound(); + } + + appDbContext.Transactions.Remove(existingTransaction); + await appDbContext.SaveChangesAsync(ct); + + return Results.NoContent(); + } +} \ No newline at end of file diff --git a/src/FiscalOS.API/Transactions/TransactionsExtensions.cs b/src/FiscalOS.API/Transactions/TransactionsExtensions.cs index d3e649c..963a62f 100644 --- a/src/FiscalOS.API/Transactions/TransactionsExtensions.cs +++ b/src/FiscalOS.API/Transactions/TransactionsExtensions.cs @@ -1,3 +1,4 @@ +using FiscalOS.API.Transactions.DeleteById; using FiscalOS.API.Transactions.FireWebhook; using FiscalOS.API.Transactions.Get; using FiscalOS.API.Transactions.Webhook; @@ -14,6 +15,7 @@ internal static class TransactionsExtensions .RequireAuthorization(); transactionsGroup.MapGetEndpoint(); + transactionsGroup.MapDeleteEndpoint(); transactionsGroup.MapWebhookEndpoint().AllowAnonymous(); if (app.Environment.IsProduction() is false) diff --git a/tests/FiscalOS.API.Tests/Integration/Transactions/DeleteByIdTests.cs b/tests/FiscalOS.API.Tests/Integration/Transactions/DeleteByIdTests.cs new file mode 100644 index 0000000..f7107a5 --- /dev/null +++ b/tests/FiscalOS.API.Tests/Integration/Transactions/DeleteByIdTests.cs @@ -0,0 +1,120 @@ +using Transaction = FiscalOS.Core.Transactions.Transaction; + +namespace FiscalOS.API.Tests.Integration.Transactions; + +public class DeleteByIdTests(TestApi testApi) : IntegrationTest(testApi) +{ + private static Uri GetDeleteByIdUri(Guid id) + { + return new($"/transactions/{id}", UriKind.Relative); + } + + [Fact] + public async Task DeleteById_WhenCalledAndUnauthenticated_ItShouldREturn401WithProblemDetails() + { + using var request = HttpRequestBuilder.New() + .Delete(GetDeleteByIdUri(Guid.NewGuid())) + .Build(); + + var response = await Client.SendAsync(request, TestContext.Current.CancellationToken); + + await response.Should().BeProblemDetails(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task DeleteById_WhenCalledWithNonExistentTransaction_ItShouldReturn401WithProblemDetails() + { + using var request = HttpRequestBuilder.New() + .Delete(GetDeleteByIdUri(Guid.NewGuid())) + .WithUserId(Guid.NewGuid()) + .Build(); + + var response = await Client.SendAsync(request, TestContext.Current.CancellationToken); + + await response.Should().BeProblemDetails(HttpStatusCode.NotFound); + } + + [Fact] + public async Task DeleteById_WhenCalledWithTransactionIdThatDoesNotBelongToTheUser_ItShouldReturn404WithProblemDetails() + { + var user = await Api.ExecuteAsync(static async (context, ct, sp) => + { + var passwordHasher = sp.GetRequiredService(); + var encryptor = sp.GetRequiredService(); + + var user = UserBuilder.Create() + .WithInstitution(static ib => + { + ib.WithMetadata(); + ib.WithAccount(static ab => + { + ab.WithMetadata(); + ab.WithTransaction(static tb => + { + tb.WithMetadata(); + }); + }); + }) + .Build(); + + await context.AddAsync(user, ct); + await context.SaveChangesAsync(ct); + return user; + }, TestContext.Current.CancellationToken); + + using var request = HttpRequestBuilder.New() + .Delete(GetDeleteByIdUri(Guid.NewGuid())) + .WithUserId(Guid.NewGuid()) + .Build(); + + var response = await Client.SendAsync(request, TestContext.Current.CancellationToken); + + await response.Should().BeProblemDetails(HttpStatusCode.NotFound); + } + + [Fact] + public async Task DeleteById_WhenCalledWithTransactionIdThatDoesExistAndBelongsToTheUser_ItShouldReturn204WithNoContent() + { + var user = await Api.ExecuteAsync(static async (context, ct, sp) => + { + var passwordHasher = sp.GetRequiredService(); + var encryptor = sp.GetRequiredService(); + + var user = UserBuilder.Create() + .WithInstitution(static ib => + { + ib.WithMetadata(); + ib.WithAccount(static ab => + { + ab.WithMetadata(); + ab.WithTransaction(static tb => + { + tb.WithMetadata(); + }); + }); + }) + .Build(); + + await context.AddAsync(user, ct); + await context.SaveChangesAsync(ct); + return user; + }, TestContext.Current.CancellationToken); + + using var request = HttpRequestBuilder.New() + .Delete(GetDeleteByIdUri(user.Transactions.First().Id)) + .WithUserId(user.Id) + .Build(); + + var response = await Client.SendAsync(request, TestContext.Current.CancellationToken); + + response.StatusCode.Should().Be(HttpStatusCode.NoContent); + + var deletedTransaction = await Api.ExecuteAsync(async (context, ct) => + { + return await context.Set() + .SingleOrDefaultAsync(t => t.Id == user.Transactions.First().Id, ct); + }, TestContext.Current.CancellationToken); + + deletedTransaction.Should().BeNull(); + } +} \ No newline at end of file