mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-07-16 17:47:22 +00:00
Migrate Breweries to a vertical slice (Features.Breweries)
Moves brewery CRUD from the Service.Breweries/Infrastructure.Repository layered split into a single Features.Breweries project: MediatR commands/queries replace BreweryService, and the repository moves in alongside its own controller. Extracts Infrastructure.Sql out of Infrastructure.Repository (just the generic ADO.NET connection/base-repo plumbing) since Breweries no longer needs the rest of that project, while Auth and UserAccount repos still do until their own migration. Also fixes the API.Core and Infrastructure.Repository.Tests Dockerfiles, which were restoring against COPY destinations that didn't match the projects' actual relative paths (silently working only because of the COPY . . that followed); both now restore against Core.slnx with correctly nested paths, verified with a real docker build.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
using Domain.Entities;
|
||||
using FluentAssertions;
|
||||
using Features.Breweries.Commands.CreateBrewery;
|
||||
using Features.Breweries.Repository;
|
||||
using Moq;
|
||||
|
||||
namespace Features.Breweries.Tests.Commands;
|
||||
|
||||
public class CreateBreweryHandlerTests
|
||||
{
|
||||
private readonly Mock<IBreweryRepository> _repoMock = new();
|
||||
private readonly CreateBreweryHandler _handler;
|
||||
|
||||
public CreateBreweryHandlerTests()
|
||||
{
|
||||
_handler = new CreateBreweryHandler(_repoMock.Object);
|
||||
}
|
||||
|
||||
private static CreateBreweryLocation ValidLocation() =>
|
||||
new(
|
||||
CityId: Guid.NewGuid(),
|
||||
AddressLine1: "123 Main St",
|
||||
AddressLine2: null,
|
||||
PostalCode: "12345",
|
||||
Coordinates: null
|
||||
);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PersistsEntity_WithNewIdsAndCreatedAt()
|
||||
{
|
||||
var command = new CreateBreweryCommand(
|
||||
PostedById: Guid.NewGuid(),
|
||||
BreweryName: "MyBrew",
|
||||
Description: "Desc",
|
||||
Location: ValidLocation()
|
||||
);
|
||||
|
||||
BreweryPost? persisted = null;
|
||||
_repoMock
|
||||
.Setup(r => r.CreateAsync(It.IsAny<BreweryPost>()))
|
||||
.Callback<BreweryPost>(b => persisted = b)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var before = DateTime.UtcNow;
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
var after = DateTime.UtcNow;
|
||||
|
||||
persisted.Should().NotBeNull();
|
||||
persisted!.BreweryPostId.Should().NotBe(Guid.Empty);
|
||||
persisted.PostedById.Should().Be(command.PostedById);
|
||||
persisted.BreweryName.Should().Be("MyBrew");
|
||||
persisted.Description.Should().Be("Desc");
|
||||
persisted.CreatedAt.Should().BeOnOrAfter(before).And.BeOnOrBefore(after);
|
||||
persisted.UpdatedAt.Should().BeNull();
|
||||
|
||||
persisted.Location.Should().NotBeNull();
|
||||
persisted.Location!.BreweryPostLocationId.Should().NotBe(Guid.Empty);
|
||||
persisted.Location.BreweryPostLocationId.Should().NotBe(persisted.BreweryPostId);
|
||||
persisted.Location.CityId.Should().Be(command.Location.CityId);
|
||||
persisted.Location.AddressLine1.Should().Be(command.Location.AddressLine1);
|
||||
persisted.Location.PostalCode.Should().Be(command.Location.PostalCode);
|
||||
|
||||
_repoMock.Verify(r => r.CreateAsync(It.IsAny<BreweryPost>()), Times.Once);
|
||||
|
||||
result.BreweryName.Should().Be("MyBrew");
|
||||
result.Location.Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Features.Breweries.Commands.DeleteBrewery;
|
||||
using Features.Breweries.Repository;
|
||||
using Moq;
|
||||
|
||||
namespace Features.Breweries.Tests.Commands;
|
||||
|
||||
public class DeleteBreweryHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Handle_DelegatesToRepository()
|
||||
{
|
||||
var repoMock = new Mock<IBreweryRepository>();
|
||||
var handler = new DeleteBreweryHandler(repoMock.Object);
|
||||
var id = Guid.NewGuid();
|
||||
repoMock.Setup(r => r.DeleteAsync(id)).Returns(Task.CompletedTask);
|
||||
|
||||
await handler.Handle(new DeleteBreweryCommand(id), CancellationToken.None);
|
||||
|
||||
repoMock.Verify(r => r.DeleteAsync(id), Times.Once);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using Domain.Entities;
|
||||
using FluentAssertions;
|
||||
using Features.Breweries.Commands.UpdateBrewery;
|
||||
using Features.Breweries.Repository;
|
||||
using Moq;
|
||||
|
||||
namespace Features.Breweries.Tests.Commands;
|
||||
|
||||
public class UpdateBreweryHandlerTests
|
||||
{
|
||||
private readonly Mock<IBreweryRepository> _repoMock = new();
|
||||
private readonly UpdateBreweryHandler _handler;
|
||||
|
||||
public UpdateBreweryHandlerTests()
|
||||
{
|
||||
_handler = new UpdateBreweryHandler(_repoMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UpdatesNameDescription_AndSetsUpdatedAt()
|
||||
{
|
||||
var command = new UpdateBreweryCommand(
|
||||
BreweryPostId: Guid.NewGuid(),
|
||||
PostedById: Guid.NewGuid(),
|
||||
BreweryName: "Renamed",
|
||||
Description: "New description",
|
||||
Location: null
|
||||
);
|
||||
|
||||
BreweryPost? persisted = null;
|
||||
_repoMock
|
||||
.Setup(r => r.UpdateAsync(It.IsAny<BreweryPost>()))
|
||||
.Callback<BreweryPost>(b => persisted = b)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var before = DateTime.UtcNow;
|
||||
await _handler.Handle(command, CancellationToken.None);
|
||||
var after = DateTime.UtcNow;
|
||||
|
||||
persisted.Should().NotBeNull();
|
||||
persisted!.BreweryPostId.Should().Be(command.BreweryPostId);
|
||||
persisted.PostedById.Should().Be(command.PostedById);
|
||||
persisted.BreweryName.Should().Be("Renamed");
|
||||
persisted.Description.Should().Be("New description");
|
||||
persisted.UpdatedAt.Should().NotBeNull();
|
||||
persisted.UpdatedAt!.Value.Should().BeOnOrAfter(before).And.BeOnOrBefore(after);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ClearsLocation_WhenCommandLocationIsNull()
|
||||
{
|
||||
var command = new UpdateBreweryCommand(
|
||||
BreweryPostId: Guid.NewGuid(),
|
||||
PostedById: Guid.NewGuid(),
|
||||
BreweryName: "Name",
|
||||
Description: "Description",
|
||||
Location: null
|
||||
);
|
||||
|
||||
BreweryPost? persisted = null;
|
||||
_repoMock
|
||||
.Setup(r => r.UpdateAsync(It.IsAny<BreweryPost>()))
|
||||
.Callback<BreweryPost>(b => persisted = b)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
persisted!.Location.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_SetsLocation_WhenCommandLocationProvided()
|
||||
{
|
||||
var locationCommand = new UpdateBreweryLocation(
|
||||
BreweryPostLocationId: Guid.NewGuid(),
|
||||
CityId: Guid.NewGuid(),
|
||||
AddressLine1: "456 Oak Ave",
|
||||
AddressLine2: "Suite 2",
|
||||
PostalCode: "54321",
|
||||
Coordinates: null
|
||||
);
|
||||
var command = new UpdateBreweryCommand(
|
||||
BreweryPostId: Guid.NewGuid(),
|
||||
PostedById: Guid.NewGuid(),
|
||||
BreweryName: "Name",
|
||||
Description: "Description",
|
||||
Location: locationCommand
|
||||
);
|
||||
|
||||
BreweryPost? persisted = null;
|
||||
_repoMock
|
||||
.Setup(r => r.UpdateAsync(It.IsAny<BreweryPost>()))
|
||||
.Callback<BreweryPost>(b => persisted = b)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
persisted!.Location.Should().NotBeNull();
|
||||
persisted.Location!.BreweryPostLocationId.Should().Be(locationCommand.BreweryPostLocationId);
|
||||
persisted.Location.BreweryPostId.Should().Be(command.BreweryPostId);
|
||||
persisted.Location.CityId.Should().Be(locationCommand.CityId);
|
||||
persisted.Location.AddressLine1.Should().Be(locationCommand.AddressLine1);
|
||||
persisted.Location.AddressLine2.Should().Be(locationCommand.AddressLine2);
|
||||
persisted.Location.PostalCode.Should().Be(locationCommand.PostalCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>Features.Breweries.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.9.0" />
|
||||
<PackageReference Include="DbMocker" Version="1.26.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Features.Breweries\Features.Breweries.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,46 @@
|
||||
using Domain.Entities;
|
||||
using FluentAssertions;
|
||||
using Features.Breweries.Queries.GetAllBreweries;
|
||||
using Features.Breweries.Repository;
|
||||
using Moq;
|
||||
|
||||
namespace Features.Breweries.Tests.Queries;
|
||||
|
||||
public class GetAllBreweriesHandlerTests
|
||||
{
|
||||
private readonly Mock<IBreweryRepository> _repoMock = new();
|
||||
private readonly GetAllBreweriesHandler _handler;
|
||||
|
||||
public GetAllBreweriesHandlerTests()
|
||||
{
|
||||
_handler = new GetAllBreweriesHandler(_repoMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PassesLimitAndOffset_ToRepository()
|
||||
{
|
||||
_repoMock.Setup(r => r.GetAllAsync(10, 5))
|
||||
.ReturnsAsync(Array.Empty<BreweryPost>());
|
||||
|
||||
var result = await _handler.Handle(new GetAllBreweriesQuery(10, 5), CancellationToken.None);
|
||||
|
||||
result.Should().BeEmpty();
|
||||
_repoMock.Verify(r => r.GetAllAsync(10, 5), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsAllBreweries_FromRepository()
|
||||
{
|
||||
var breweries = new[]
|
||||
{
|
||||
new BreweryPost { BreweryPostId = Guid.NewGuid(), BreweryName = "A" },
|
||||
new BreweryPost { BreweryPostId = Guid.NewGuid(), BreweryName = "B" },
|
||||
};
|
||||
_repoMock.Setup(r => r.GetAllAsync(null, null))
|
||||
.ReturnsAsync(breweries);
|
||||
|
||||
var result = await _handler.Handle(new GetAllBreweriesQuery(null, null), CancellationToken.None);
|
||||
|
||||
result.Select(b => b.BreweryPostId).Should().BeEquivalentTo(breweries.Select(b => b.BreweryPostId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Domain.Entities;
|
||||
using FluentAssertions;
|
||||
using Features.Breweries.Queries.GetBreweryById;
|
||||
using Features.Breweries.Repository;
|
||||
using Moq;
|
||||
|
||||
namespace Features.Breweries.Tests.Queries;
|
||||
|
||||
public class GetBreweryByIdHandlerTests
|
||||
{
|
||||
private readonly Mock<IBreweryRepository> _repoMock = new();
|
||||
private readonly GetBreweryByIdHandler _handler;
|
||||
|
||||
public GetBreweryByIdHandlerTests()
|
||||
{
|
||||
_handler = new GetBreweryByIdHandler(_repoMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsBrewery_WhenFound()
|
||||
{
|
||||
var brewery = new BreweryPost { BreweryPostId = Guid.NewGuid(), BreweryName = "Test" };
|
||||
_repoMock.Setup(r => r.GetByIdAsync(brewery.BreweryPostId))
|
||||
.ReturnsAsync(brewery);
|
||||
|
||||
var result = await _handler.Handle(new GetBreweryByIdQuery(brewery.BreweryPostId), CancellationToken.None);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.BreweryPostId.Should().Be(brewery.BreweryPostId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsNull_WhenNotFound()
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
_repoMock.Setup(r => r.GetByIdAsync(id))
|
||||
.ReturnsAsync((BreweryPost?)null);
|
||||
|
||||
var result = await _handler.Handle(new GetBreweryByIdQuery(id), CancellationToken.None);
|
||||
|
||||
result.Should().BeNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using Apps72.Dev.Data.DbMocker;
|
||||
using FluentAssertions;
|
||||
using Features.Breweries.Repository;
|
||||
using Domain.Entities;
|
||||
|
||||
namespace Features.Breweries.Tests.Repository;
|
||||
|
||||
public class BreweryRepositoryTests
|
||||
{
|
||||
private static BreweryRepository CreateRepo(MockDbConnection conn) =>
|
||||
new(new TestConnectionFactory(conn));
|
||||
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_ReturnsBrewery_WhenExists()
|
||||
{
|
||||
var breweryId = Guid.NewGuid();
|
||||
var conn = new MockDbConnection();
|
||||
|
||||
// Repository calls the stored procedure
|
||||
const string getByIdSql = "USP_GetBreweryById";
|
||||
|
||||
var locationId = Guid.NewGuid();
|
||||
|
||||
conn.Mocks.When(cmd => cmd.CommandText == getByIdSql)
|
||||
.ReturnsTable(
|
||||
MockTable
|
||||
.WithColumns(
|
||||
("BreweryPostId", typeof(Guid)),
|
||||
("PostedById", typeof(Guid)),
|
||||
("BreweryName", typeof(string)),
|
||||
("Description", typeof(string)),
|
||||
("CreatedAt", typeof(DateTime)),
|
||||
("UpdatedAt", typeof(DateTime?)),
|
||||
("Timer", typeof(byte[])),
|
||||
("BreweryPostLocationId", typeof(Guid)),
|
||||
("CityId", typeof(Guid)),
|
||||
("AddressLine1", typeof(string)),
|
||||
("AddressLine2", typeof(string)),
|
||||
("PostalCode", typeof(string)),
|
||||
("Coordinates", typeof(byte[]))
|
||||
)
|
||||
.AddRow(
|
||||
breweryId,
|
||||
Guid.NewGuid(),
|
||||
"Test Brewery",
|
||||
"A test brewery description",
|
||||
DateTime.UtcNow,
|
||||
null,
|
||||
null,
|
||||
locationId,
|
||||
Guid.NewGuid(),
|
||||
"123 Main St",
|
||||
null,
|
||||
"12345",
|
||||
null
|
||||
)
|
||||
);
|
||||
|
||||
var repo = CreateRepo(conn);
|
||||
var result = await repo.GetByIdAsync(breweryId);
|
||||
result.Should().NotBeNull();
|
||||
result!.BreweryPostId.Should().Be(breweryId);
|
||||
result.Location.Should().NotBeNull();
|
||||
result.Location!.BreweryPostLocationId.Should().Be(locationId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_ReturnsNull_WhenNotExists()
|
||||
{
|
||||
var conn = new MockDbConnection();
|
||||
conn.Mocks.When(cmd => cmd.CommandText == "USP_GetBreweryById")
|
||||
.ReturnsTable(MockTable.Empty());
|
||||
var repo = CreateRepo(conn);
|
||||
var result = await repo.GetByIdAsync(Guid.NewGuid());
|
||||
result.Should().BeNull();
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_ExecutesSuccessfully()
|
||||
{
|
||||
var conn = new MockDbConnection();
|
||||
conn.Mocks.When(cmd => cmd.CommandText == "USP_CreateBrewery")
|
||||
.ReturnsScalar(1);
|
||||
var repo = CreateRepo(conn);
|
||||
var brewery = new BreweryPost
|
||||
{
|
||||
BreweryPostId = Guid.NewGuid(),
|
||||
PostedById = Guid.NewGuid(),
|
||||
BreweryName = "Test Brewery",
|
||||
Description = "A test brewery description",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Location = new BreweryPostLocation
|
||||
{
|
||||
BreweryPostLocationId = Guid.NewGuid(),
|
||||
CityId = Guid.NewGuid(),
|
||||
AddressLine1 = "123 Main St",
|
||||
PostalCode = "12345",
|
||||
Coordinates = [0x00, 0x01]
|
||||
}
|
||||
};
|
||||
|
||||
// Should not throw
|
||||
var act = async () => await repo.CreateAsync(brewery);
|
||||
await act.Should().NotThrowAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Data.Common;
|
||||
using Infrastructure.Sql;
|
||||
|
||||
namespace Features.Breweries.Tests.Repository;
|
||||
|
||||
internal class TestConnectionFactory(DbConnection conn) : ISqlConnectionFactory
|
||||
{
|
||||
private readonly DbConnection _conn = conn;
|
||||
|
||||
public DbConnection CreateConnection() => _conn;
|
||||
}
|
||||
Reference in New Issue
Block a user