From 56c83db207a4dc958e9cd265ae7fb3940fea2f50 Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Sun, 29 Mar 2026 15:41:23 -0400 Subject: [PATCH] Implement brewery repo, SQL procs and tests --- .../05-Breweries/USP_CreateBrewery.sql | 21 ++-- .../05-Breweries/USP_GetBreweryById.sql | 9 ++ .../Domain.Entities/Entities/BreweryPost.cs | 1 + .../Breweries/BreweryRepository.test.cs | 108 ++++++++++++++++ .../Auth/AuthRepository.cs | 11 +- .../Breweries/BreweryRepository.cs | 116 +++++++++++++++--- 6 files changed, 228 insertions(+), 38 deletions(-) create mode 100644 src/Core/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_GetBreweryById.sql create mode 100644 src/Core/Infrastructure/Infrastructure.Repository.Tests/Breweries/BreweryRepository.test.cs diff --git a/src/Core/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_CreateBrewery.sql b/src/Core/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_CreateBrewery.sql index b43896e..3683dc7 100644 --- a/src/Core/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_CreateBrewery.sql +++ b/src/Core/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_CreateBrewery.sql @@ -20,28 +20,31 @@ BEGIN THROW 50002, 'Brewery description cannot be null.', 1; IF NOT EXISTS (SELECT 1 - FROM dbo.UserAccount - WHERE UserAccountID = @PostedByID) + FROM dbo.UserAccount + WHERE UserAccountID = @PostedByID) THROW 50404, 'User not found.', 1; IF NOT EXISTS (SELECT 1 - FROM dbo.City - WHERE CityID = @CityID) + FROM dbo.City + WHERE CityID = @CityID) THROW 50404, 'City not found.', 1; DECLARE @NewBreweryID UNIQUEIDENTIFIER = NEWID(); + DECLARE @NewBrewerLocationID UNIQUEIDENTIFIER = NEWID(); BEGIN TRANSACTION; INSERT INTO dbo.BreweryPost (BreweryPostID, BreweryName, Description, PostedByID) - VALUES - (@NewBreweryID, @BreweryName, @Description, @PostedByID); + VALUES (@NewBreweryID, @BreweryName, @Description, @PostedByID); INSERT INTO dbo.BreweryPostLocation - (BreweryPostID, CityID, AddressLine1, AddressLine2, PostalCode, Coordinates) - VALUES - (@NewBreweryID, @CityID, @AddressLine1, @AddressLine2, @PostalCode, @Coordinates); + (BreweryPostLocationID, BreweryPostID, CityID, AddressLine1, AddressLine2, PostalCode, Coordinates) + VALUES (@NewBrewerLocationID, @NewBreweryID, @CityID, @AddressLine1, @AddressLine2, @PostalCode, @Coordinates); COMMIT TRANSACTION; + + SELECT @NewBreweryID AS BreweryPostID, + @NewBrewerLocationID AS BreweryPostLocationID; + END diff --git a/src/Core/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_GetBreweryById.sql b/src/Core/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_GetBreweryById.sql new file mode 100644 index 0000000..25425ef --- /dev/null +++ b/src/Core/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_GetBreweryById.sql @@ -0,0 +1,9 @@ +CREATE OR ALTER PROCEDURE dbo.USP_GetBreweryById @BreweryPostID UNIQUEIDENTIFIER +AS +BEGIN + SELECT * + FROM BreweryPost bp + INNER JOIN BreweryPostLocation bpl + ON bp.BreweryPostID = bpl.BreweryPostID + WHERE bp.BreweryPostID = @BreweryPostID; +END \ No newline at end of file diff --git a/src/Core/Domain/Domain.Entities/Entities/BreweryPost.cs b/src/Core/Domain/Domain.Entities/Entities/BreweryPost.cs index 41426f5..07a7b78 100644 --- a/src/Core/Domain/Domain.Entities/Entities/BreweryPost.cs +++ b/src/Core/Domain/Domain.Entities/Entities/BreweryPost.cs @@ -9,4 +9,5 @@ public class BreweryPost public DateTime CreatedAt { get; set; } public DateTime? UpdatedAt { get; set; } public byte[]? Timer { get; set; } + public BreweryPostLocation? Location { get; set; } } diff --git a/src/Core/Infrastructure/Infrastructure.Repository.Tests/Breweries/BreweryRepository.test.cs b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Breweries/BreweryRepository.test.cs new file mode 100644 index 0000000..6352124 --- /dev/null +++ b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Breweries/BreweryRepository.test.cs @@ -0,0 +1,108 @@ +using Apps72.Dev.Data.DbMocker; +using FluentAssertions; +using Infrastructure.Repository.Breweries; +using Infrastructure.Repository.Tests.Database; +using Domain.Entities; + +namespace Infrastructure.Repository.Tests.Breweries; + +public class BreweryRepositoryTest +{ + 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(); + } +} diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs index fbff2b3..b878446 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs +++ b/src/Core/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs @@ -35,16 +35,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) var result = await command.ExecuteScalarAsync(); var userAccountId = result != null ? (Guid)result : Guid.Empty; - return new Domain.Entities.UserAccount - { - UserAccountId = userAccountId, - Username = username, - FirstName = firstName, - LastName = lastName, - Email = email, - DateOfBirth = dateOfBirth, - CreatedAt = DateTime.UtcNow, - }; + return await GetUserByIdAsync(userAccountId) ?? throw new Exception("Failed to retrieve newly registered user."); } public async Task GetUserByEmailAsync( diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Breweries/BreweryRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Breweries/BreweryRepository.cs index f5fe035..82177fc 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/Breweries/BreweryRepository.cs +++ b/src/Core/Infrastructure/Infrastructure.Repository/Breweries/BreweryRepository.cs @@ -13,15 +13,31 @@ public interface IBreweryRepository Task CreateAsync(BreweryPost brewery); } -public class BreweryRepository(ISqlConnectionFactory connectionFactory) - : Repository(connectionFactory), - IBreweryRepository +public class BreweryRepository : Repository, IBreweryRepository { - private static ISqlConnectionFactory? _connectionFactory; + private readonly ISqlConnectionFactory _connectionFactory; - public Task GetByIdAsync(Guid id) + public BreweryRepository(ISqlConnectionFactory connectionFactory) + : base(connectionFactory) { - throw new NotImplementedException(); + _connectionFactory = connectionFactory; + } + + public async Task GetByIdAsync(Guid id) + { + await using var connection = await CreateConnection(); + await using var command = connection.CreateCommand(); + command.CommandType = System.Data.CommandType.StoredProcedure; + + command.CommandText = "USP_GetBreweryById"; + AddParameter(command, "@BreweryPostID", id); + + await using var reader = await command.ExecuteReaderAsync(); + if (await reader.ReadAsync()) + { + return MapToEntity(reader); + } + return null; } public Task> GetAllAsync(int? limit, int? offset) @@ -39,29 +55,96 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory) throw new NotImplementedException(); } - public async Task CreateAsync(BreweryPost brewery, BreweryPostLocation location) + public async Task CreateAsync(BreweryPost brewery) { await using var connection = await CreateConnection(); await using var command = connection.CreateCommand(); - command.CommandText = "USP_CreateBreweryPost"; + command.CommandText = "USP_CreateBrewery"; command.CommandType = System.Data.CommandType.StoredProcedure; + if (brewery.Location is null) + { + throw new ArgumentException("Location must be provided when creating a brewery."); + } + AddParameter(command, "@BreweryName", brewery.BreweryName); AddParameter(command, "@Description", brewery.Description); AddParameter(command, "@PostedByID", brewery.PostedById); - AddParameter(command, "@CityID", location.CityId); - AddParameter(command, "@AddressLine1", location.AddressLine1); - AddParameter(command, "@AddressLine2", location.AddressLine2); - AddParameter(command, "@PostalCode", location.PostalCode); - AddParameter(command, "@Coordinates", location.Coordinates); + AddParameter(command, "@CityID", brewery.Location?.CityId); + AddParameter(command, "@AddressLine1", brewery.Location?.AddressLine1); + AddParameter(command, "@AddressLine2", brewery.Location?.AddressLine2); + AddParameter(command, "@PostalCode", brewery.Location?.PostalCode); + AddParameter(command, "@Coordinates", brewery.Location?.Coordinates); await command.ExecuteNonQueryAsync(); } protected override BreweryPost MapToEntity(DbDataReader reader) { - throw new NotImplementedException(); + var brewery = new BreweryPost(); + + var ordBreweryPostId = reader.GetOrdinal("BreweryPostId"); + var ordPostedById = reader.GetOrdinal("PostedById"); + var ordBreweryName = reader.GetOrdinal("BreweryName"); + var ordDescription = reader.GetOrdinal("Description"); + var ordCreatedAt = reader.GetOrdinal("CreatedAt"); + var ordUpdatedAt = reader.GetOrdinal("UpdatedAt"); + var ordTimer = reader.GetOrdinal("Timer"); + + brewery.BreweryPostId = reader.GetGuid(ordBreweryPostId); + brewery.PostedById = reader.GetGuid(ordPostedById); + brewery.BreweryName = reader.GetString(ordBreweryName); + brewery.Description = reader.GetString(ordDescription); + brewery.CreatedAt = reader.GetDateTime(ordCreatedAt); + + brewery.UpdatedAt = reader.IsDBNull(ordUpdatedAt) ? null : reader.GetDateTime(ordUpdatedAt); + + // Read timer (varbinary/rowversion) robustly + if (reader.IsDBNull(ordTimer)) + { + brewery.Timer = null; + } + else + { + try + { + brewery.Timer = reader.GetFieldValue(ordTimer); + } + catch + { + var length = reader.GetBytes(ordTimer, 0, null, 0, 0); + var buffer = new byte[length]; + reader.GetBytes(ordTimer, 0, buffer, 0, (int)length); + brewery.Timer = buffer; + } + } + + // Map BreweryPostLocation if columns are present + try + { + var ordLocationId = reader.GetOrdinal("BreweryPostLocationId"); + if (!reader.IsDBNull(ordLocationId)) + { + var location = new BreweryPostLocation + { + BreweryPostLocationId = reader.GetGuid(ordLocationId), + BreweryPostId = reader.GetGuid(reader.GetOrdinal("BreweryPostId")), + CityId = reader.GetGuid(reader.GetOrdinal("CityId")), + AddressLine1 = reader.GetString(reader.GetOrdinal("AddressLine1")), + AddressLine2 = reader.IsDBNull(reader.GetOrdinal("AddressLine2")) ? null : reader.GetString(reader.GetOrdinal("AddressLine2")), + PostalCode = reader.GetString(reader.GetOrdinal("PostalCode")), + Coordinates = reader.IsDBNull(reader.GetOrdinal("Coordinates")) ? null : reader.GetFieldValue(reader.GetOrdinal("Coordinates")) + }; + brewery.Location = location; + } + } + catch (IndexOutOfRangeException) + { + // Location columns not present, skip mapping location + } + + return brewery; } private static void AddParameter( @@ -75,9 +158,4 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory) p.Value = value ?? DBNull.Value; command.Parameters.Add(p); } - - public Task CreateAsync(BreweryPost brewery) - { - throw new NotImplementedException(); - } }