Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;

using Microsoft.EntityFrameworkCore;

using Library.API.Domain.Models;
Expand All @@ -22,7 +23,6 @@ public async Task AddAsync(BookOrder bookOrder)
{
using var activity = _activitySource.StartActivity("Repository: BookOrderRepository.AddAsync");
await _context.BookOrders.AddAsync(bookOrder);
await _context.SaveChangesAsync();
}

public async Task<BookOrder?> FindByIdAsync(int id)
Expand Down
10 changes: 10 additions & 0 deletions src/Library.API/Infrastructure/Services/BookOrderService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ namespace Library.API.Infrastructure.Services;
public class BookOrderService : BaseService, IBookOrderService
{
private readonly IBookOrderRepository _bookOrderRepository;
private readonly IBookRepository _bookRepository;
private readonly ActivitySource _activitySource;
private readonly IPublishEndpoint _publishEndpoint;
private readonly IMapper _mapper;

public BookOrderService(
IBookOrderRepository bookOrderRepository,
IBookRepository bookRepository,
IUnitOfWork unitOfWork,
ILogger<BookOrderService> logger,
ActivitySource activitySource,
Expand All @@ -30,6 +32,7 @@ public BookOrderService(
: base(unitOfWork, logger)
{
_bookOrderRepository = bookOrderRepository;
_bookRepository = bookRepository;
_activitySource = activitySource;
_mapper = mapper;
_publishEndpoint = publishEndpoint;
Expand Down Expand Up @@ -62,6 +65,13 @@ public async Task<ServiceResponse> AddAsync(BookOrder bookOrder)
if (bookOrder.Items.Count == 0)
return ServiceResponse.Fail("Book order must have at least one item", ErrorType.ValidationError);

foreach (var item in bookOrder.Items)
{
var book = await _bookRepository.FindByIdAsync(item.BookId);
if (book == null)
return ServiceResponse.Fail($"Book with id {item.BookId} was not found", ErrorType.ValidationError);
}

await _bookOrderRepository.AddAsync(bookOrder);
await _unitOfWork.CompleteAsync();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MassTransit;

using Library.API.Domain.Models;
using Library.API.Domain.Services;
using Library.Events.Messages;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MassTransit;

using Library.API.Domain.Models;
using Library.API.Domain.Services;
using Library.Events.Messages;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MassTransit;

using Library.API.Domain.Models;
using Library.API.Domain.Services;
using Library.Events.Messages;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MassTransit;

using Library.API.Domain.Models;
using Library.API.Domain.Services;
using Library.Events.Messages;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MassTransit;

using Library.API.Domain.Models;
using Library.API.Domain.Services;
using Library.Events.Messages;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MassTransit;

using Library.API.Domain.Models;
using Library.API.Domain.Services;
using Library.Events.Messages;
Expand Down
8 changes: 0 additions & 8 deletions src/Library.Events/Messages/PaymentRequestedEvent.cs

This file was deleted.

180 changes: 180 additions & 0 deletions tests/Library.API.IntegrationTests/Api/CheckoutControllerTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
using System.Net;
using System.Net.Http.Json;

using FluentAssertions;

using Library.API.DTOs;
using Library.API.DTOs.Response;
using Library.API.IntegrationTests.Fixtures;
using Library.Events.Messages;

namespace Library.API.IntegrationTests.Api;

public class CheckoutControllerTest(LibraryApiFactory factory) : IntegrationTestBase(factory)
{

[Fact]
public async Task CreateBookOrder_ShouldReturnSuccess_AndPublishOrderPlacedEvent()
{
// Arrange
TestDataHelper.SeedAuthors(Factory, true);
TestDataHelper.SeedBooks(Factory);
var endpoint = "/api/checkout";
var newBookOrder = new SaveBookOrderDto
{
Items = [
new SaveBookOrderItemDto { BookId = 1, Quantity = 1 }
]
};

// Act
var response = await Client.PostAsJsonAsync(endpoint, newBookOrder, CancellationToken);
var apiResponse = await response.Content.ReadFromJsonAsync<ApiResponse<BookOrderDto>>(CancellationToken);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
apiResponse.Should().NotBeNull();
apiResponse.Data.Should().NotBeNull();
apiResponse.Data.Id.Should().Be(1);
apiResponse.Data.Items.Should().HaveCount(1);
apiResponse.Data.Items[0].BookId.Should().Be(1);
apiResponse.Data.Items[0].Quantity.Should().Be(1);

VerifyEventPublished<OrderPlacedEvent>();
}

[Fact]
public async Task CreateBookOrder_WithInvalidData_ShouldReturnBadRequest_AndNotPublishEvent()
{
// Arrange
var endpoint = "/api/checkout";
var newBookOrder = new SaveBookOrderDto { Items = [] };

// Act
var response = await Client.PostAsJsonAsync(endpoint, newBookOrder, CancellationToken);
var apiResponse = await response.Content.ReadFromJsonAsync<ApiProblemDetails>(CancellationToken);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
apiResponse.Should().NotBeNull();
apiResponse.Title.Should().Be("Validation Error");
apiResponse.Status.Should().Be((int)HttpStatusCode.BadRequest);
apiResponse.Type.Should().Be("https://tools.ietf.org/html/rfc7231#section-6.5.1");
apiResponse.Detail.Should().Be("Book order must have at least one item");
apiResponse.Instance.Should().Be(endpoint);

VerifyEventNotPublished<OrderPlacedEvent>();
}

[Fact]
public async Task CreateBookOrder_WithInvalidBookId_ShouldReturnBadRequest()
{
// Arrange
var endpoint = "/api/checkout";
var newBookOrder = new SaveBookOrderDto
{
Items = [
new SaveBookOrderItemDto { BookId = 999, Quantity = 1 }
]
};

// Act
var response = await Client.PostAsJsonAsync(endpoint, newBookOrder, CancellationToken);
var apiResponse = await response.Content.ReadFromJsonAsync<ApiProblemDetails>(CancellationToken);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
apiResponse.Should().NotBeNull();
apiResponse.Title.Should().Be("Validation Error");
apiResponse.Status.Should().Be((int)HttpStatusCode.BadRequest);
apiResponse.Type.Should().Be("https://tools.ietf.org/html/rfc7231#section-6.5.1");
apiResponse.Detail.Should().Be("Book with id 999 was not found");
apiResponse.Instance.Should().Be(endpoint);

VerifyEventNotPublished<OrderPlacedEvent>();
}

[Fact]
public async Task CreateBookOrder_WithInvalidQuantity_ShouldReturnBadRequest()
{
// Arrange
var endpoint = "/api/checkout";
var newBookOrder = new SaveBookOrderDto
{
Items = [
new SaveBookOrderItemDto { BookId = 1 }
]
};

// Act
var response = await Client.PostAsJsonAsync(endpoint, newBookOrder, CancellationToken);
var apiResponse = await response.Content.ReadFromJsonAsync<ApiProblemDetails>(CancellationToken);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
apiResponse.Should().NotBeNull();
apiResponse.Title.Should().Be("Validation Error");
apiResponse.Status.Should().Be((int)HttpStatusCode.BadRequest);
apiResponse.Type.Should().Be("https://tools.ietf.org/html/rfc7231#section-6.5.1");
apiResponse.Detail.Should().Be("One or more validation errors occurred.");
apiResponse.Instance.Should().Be(endpoint);
apiResponse.Errors.Should().NotBeNull();
apiResponse.Errors.Should().HaveCount(1);
apiResponse.Errors.Should().ContainKey("Items[0].Quantity");
apiResponse.Errors["Items[0].Quantity"].Should().Contain("The field Quantity must be between 1 and 50.");

VerifyEventNotPublished<OrderPlacedEvent>();
}

[Fact]
public async Task GetBookOrder_WithValidId_ShouldReturnBookOrder()
{
// Arrange
TestDataHelper.SeedAuthors(Factory, true);
TestDataHelper.SeedBooks(Factory);
TestDataHelper.SeedBookOrders(Factory);
var orderId = 1;
var endpoint = $"/api/checkout/{orderId}";

// Act
var response = await Client.GetAsync(endpoint, CancellationToken);
var apiResponse = await response.Content.ReadFromJsonAsync<ApiResponse<BookOrderDto>>(CancellationToken);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
apiResponse.Should().NotBeNull();
apiResponse.Data.Should().NotBeNull();
apiResponse.Data.Id.Should().Be(orderId);
apiResponse.Data.CheckoutDate.Should().Be(new DateTime(2020, 1, 1));
apiResponse.Data.Status.Should().Be("Order Placed");
apiResponse.Data.Items.Should().HaveCount(1);
apiResponse.Data.Items[0].BookId.Should().Be(1);
apiResponse.Data.Items[0].Title.Should().Be("Book 1");
apiResponse.Data.Items[0].Quantity.Should().Be(1);

VerifyEventNotPublished<OrderPlacedEvent>();
}

[Fact]
public async Task GetBookOrder_WithInvalidId_ShouldReturnNotFound()
{
// Arrange
var endpoint = "/api/checkout/999";

// Act
var response = await Client.GetAsync(endpoint, CancellationToken);
var apiResponse = await response.Content.ReadFromJsonAsync<ApiProblemDetails>(CancellationToken);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
apiResponse.Should().NotBeNull();
apiResponse.Title.Should().Be("Resource Not Found");
apiResponse.Status.Should().Be((int)HttpStatusCode.NotFound);
apiResponse.Type.Should().Be("https://tools.ietf.org/html/rfc7231#section-6.5.4");
apiResponse.Detail.Should().Be("Book order with id 999 was not found");
apiResponse.Instance.Should().Be(endpoint);

VerifyEventNotPublished<OrderPlacedEvent>();
}

}
25 changes: 22 additions & 3 deletions tests/Library.API.IntegrationTests/Api/IntegrationTestBase.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
using Library.API.IntegrationTests.Fixtures;

using MassTransit;
using Microsoft.AspNetCore.Mvc.Testing;
using Moq;

using Library.API.IntegrationTests.Fixtures;

namespace Library.API.IntegrationTests.Api;

public class IntegrationTestBase : IClassFixture<LibraryApiFactory>, IAsyncLifetime
public abstract class IntegrationTestBase : IClassFixture<LibraryApiFactory>, IAsyncLifetime
{
protected readonly HttpClient Client;
protected readonly LibraryApiFactory Factory;
protected readonly Mock<IPublishEndpoint> PublishEndpointMock;
private CancellationTokenSource? _cts;
private const int TestTimeoutSeconds = 10;

protected IntegrationTestBase(LibraryApiFactory factory)
{
Factory = factory;
PublishEndpointMock = factory.GetPublishEndpointMock();
Client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
Expand All @@ -25,6 +29,7 @@ protected IntegrationTestBase(LibraryApiFactory factory)
public ValueTask InitializeAsync()
{
_cts = new CancellationTokenSource(TimeSpan.FromSeconds(TestTimeoutSeconds));
PublishEndpointMock.Reset();
return ValueTask.CompletedTask;
}

Expand All @@ -34,4 +39,18 @@ public ValueTask DisposeAsync()
_cts?.Dispose();
return ValueTask.CompletedTask;
}

protected void VerifyEventPublished<T>(Times? times = null) where T : class
{
PublishEndpointMock.Verify(
x => x.Publish(It.IsAny<T>(), It.IsAny<CancellationToken>()),
times ?? Times.Once());
}

protected void VerifyEventNotPublished<T>() where T : class
{
PublishEndpointMock.Verify(
x => x.Publish(It.IsAny<T>(), It.IsAny<CancellationToken>()),
Times.Never());
}
}
19 changes: 16 additions & 3 deletions tests/Library.API.IntegrationTests/Fixtures/LibraryApiFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,42 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using MassTransit;
using Moq;

using Library.API.Infrastructure.Contexts;

namespace Library.API.IntegrationTests.Fixtures;

public class LibraryApiFactory : WebApplicationFactory<Program>
{
private readonly string _databaseName = $"library_api_test_db_{Guid.NewGuid()}";
private Mock<IPublishEndpoint> _publishEndpointMock = new();

public Mock<IPublishEndpoint> GetPublishEndpointMock() => _publishEndpointMock;

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Testing");
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(ApiDbContext));
if (descriptor != null)
var dbDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(ApiDbContext));
if (dbDescriptor != null)
{
services.Remove(descriptor);
services.Remove(dbDescriptor);
services.AddDbContext<ApiDbContext>(options =>
{
options.UseInMemoryDatabase(_databaseName);
});
}

var massTransitDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(IPublishEndpoint));
if (massTransitDescriptor != null)
{
services.Remove(massTransitDescriptor);
services.AddSingleton(_publishEndpointMock.Object);
}

var sp = services.BuildServiceProvider();
using var scope = sp.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApiDbContext>();
Expand Down
Loading