mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-15 01:54:43 +00:00
Migrate Auth/Brewery/Location repositories to pure Dapper; rename Timer to RowVersion (#237)
* Integrate Spectre.Console into seed application, use async data loading * Replace stored-procedure repositories with Dapper-based repositories Removes the USP_* stored procedures for Auth, Location, and Brewery data access in favour of services built with Dapper. Adds a new Features.Locations slice and optimistic concurrency support for the Brewery slice. * Add new documentation for Dapper migration/database schema * Update FluentAssertions to most recent open source ver * Update XML doc comment in backend projects * Modify brewery location dto to include denormalized data * - Updated column names from 'Timer' to 'RowVersion' in schema.sql for - Introduced new BeerPost entity and updated BreweryPost and BreweryPostLocation entities to use 'RowVersion' for concurrency. - Modified UserAccount, UserCredential, UserVerification, City, StateProvince, and Country entities to use 'RowVersion' instead of 'Timer'. - Updated tests and command handlers to reflect changes in concurrency token from 'Timer' to 'RowVersion'. - Adjusted BreweryDto and BreweryDtoMapper to include 'RowVersion' and updated mapping logic accordingly. - Ensured all repository methods and interfaces are aligned with the new concurrency handling approach. * updates * Add BeerStyle entity and related properties; refactor BreweryDto to remove BeerPosts * Fix Api.Specs test failures: stale Timer column refs and missing test.user fixture * Remove repository tests, usages of registration dto used in repo * archive old stored procs/functions * update stale docs * Add instructions for generating cryptographically secure base64 secrets on macOS/Linux and Windows
This commit is contained in:
@@ -14,6 +14,9 @@ namespace API.Core.Controllers;
|
||||
[Route("error")] // required
|
||||
public class NotFoundController : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a 404 response for any request that did not match a route.
|
||||
/// </summary>
|
||||
[HttpGet("404")] //required
|
||||
public IActionResult Handle404()
|
||||
{
|
||||
|
||||
@@ -27,6 +27,8 @@ COPY ["Features/Features.Breweries/Features.Breweries.csproj", "Features/Feature
|
||||
COPY ["Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj", "Features/Features.Breweries.Tests/"]
|
||||
COPY ["Features/Features.Emails/Features.Emails.csproj", "Features/Features.Emails/"]
|
||||
COPY ["Features/Features.Emails.Tests/Features.Emails.Tests.csproj", "Features/Features.Emails.Tests/"]
|
||||
COPY ["Features/Features.Locations/Features.Locations.csproj", "Features/Features.Locations/"]
|
||||
COPY ["Features/Features.Locations.Tests/Features.Locations.Tests.csproj", "Features/Features.Locations.Tests/"]
|
||||
COPY ["Features/Features.UserManagement/Features.UserManagement.csproj", "Features/Features.UserManagement/"]
|
||||
COPY ["Features/Features.UserManagement.Tests/Features.UserManagement.Tests.csproj", "Features/Features.UserManagement.Tests/"]
|
||||
COPY ["Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj", "Infrastructure/Infrastructure.Email/"]
|
||||
|
||||
@@ -11,12 +11,12 @@ using Features.UserManagement.DependencyInjection;
|
||||
using FluentValidation;
|
||||
using FluentValidation.AspNetCore;
|
||||
using Infrastructure.Jwt;
|
||||
using Infrastructure.Sql;
|
||||
using Infrastructure.Sql.DependencyInjection;
|
||||
using Shared.Application.Behaviors;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Global Exception Filter
|
||||
// Add current controllers to the exception filter
|
||||
builder
|
||||
.Services.AddControllers(options =>
|
||||
{
|
||||
@@ -37,7 +37,7 @@ builder.Services.AddValidatorsFromAssembly(typeof(UserController).Assembly);
|
||||
builder.Services.AddValidatorsFromAssembly(typeof(AuthController).Assembly);
|
||||
builder.Services.AddFluentValidationAutoValidation();
|
||||
|
||||
// Add MediatR. Each Features.* slice's assembly is registered here as it's introduced;
|
||||
// Add MediatR.
|
||||
// ValidationBehavior runs FluentValidation validators in the MediatR pipeline for command/query handlers.
|
||||
builder.Services.AddMediatR(cfg =>
|
||||
{
|
||||
@@ -60,15 +60,14 @@ if (!builder.Environment.IsProduction())
|
||||
|
||||
// Configure Dependency Injection -------------------------------------------------------------------------------------
|
||||
|
||||
builder.Services.AddSingleton<ISqlConnectionFactory, DefaultSqlConnectionFactory>();
|
||||
builder.Services.AddInfrastructureSql();
|
||||
|
||||
builder.Services.AddFeaturesBreweries();
|
||||
builder.Services.AddFeaturesUserManagement();
|
||||
builder.Services.AddFeaturesAuth();
|
||||
builder.Services.AddFeaturesEmails();
|
||||
|
||||
// ITokenInfrastructure is registered here (not inside Features.Auth's own DI extension) because
|
||||
// JwtAuthenticationHandler, a host-level auth middleware concern, also depends on it directly.
|
||||
// ITokenInfrastructure is registered here because JwtAuthenticationHandler depends on it directly.
|
||||
builder.Services.AddScoped<ITokenInfrastructure, JwtInfrastructure>();
|
||||
|
||||
// Register the exception filter
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": ""
|
||||
"Default": ""
|
||||
},
|
||||
"Jwt": {
|
||||
"SecretKey": "",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.9.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="7.2.0" />
|
||||
<PackageReference Include="dbup" Version="5.0.41" />
|
||||
|
||||
<!-- Reqnroll core, xUnit adapter and code-behind generator -->
|
||||
|
||||
@@ -17,6 +17,8 @@ COPY ["Features/Features.Breweries/Features.Breweries.csproj", "Features/Feature
|
||||
COPY ["Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj", "Features/Features.Breweries.Tests/"]
|
||||
COPY ["Features/Features.Emails/Features.Emails.csproj", "Features/Features.Emails/"]
|
||||
COPY ["Features/Features.Emails.Tests/Features.Emails.Tests.csproj", "Features/Features.Emails.Tests/"]
|
||||
COPY ["Features/Features.Locations/Features.Locations.csproj", "Features/Features.Locations/"]
|
||||
COPY ["Features/Features.Locations.Tests/Features.Locations.Tests.csproj", "Features/Features.Locations.Tests/"]
|
||||
COPY ["Features/Features.UserManagement/Features.UserManagement.csproj", "Features/Features.UserManagement/"]
|
||||
COPY ["Features/Features.UserManagement.Tests/Features.UserManagement.Tests.csproj", "Features/Features.UserManagement.Tests/"]
|
||||
COPY ["Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj", "Infrastructure/Infrastructure.Email/"]
|
||||
|
||||
@@ -14,6 +14,7 @@ Scenario: Successful registration with valid details
|
||||
|
||||
Scenario: Registration fails with existing username
|
||||
Given the API is running
|
||||
And I have an existing account
|
||||
When I submit a registration request with values:
|
||||
| Username | FirstName | LastName | Email | DateOfBirth | Password |
|
||||
| test.user | Test | User | example@example.com | 2001-11-11 | Password1! |
|
||||
@@ -21,6 +22,7 @@ Scenario: Registration fails with existing username
|
||||
|
||||
Scenario: Registration fails with existing email
|
||||
Given the API is running
|
||||
And I have an existing account
|
||||
When I submit a registration request with values:
|
||||
| Username | FirstName | LastName | Email | DateOfBirth | Password |
|
||||
| newuser | New | User | test.user@thebiergarten.app | 1990-01-01 | Password1! |
|
||||
|
||||
@@ -83,10 +83,59 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
);
|
||||
}
|
||||
|
||||
private const string FixtureUsername = "test.user";
|
||||
private const string FixturePassword = "Password1!";
|
||||
private const string FixtureEmail = "test.user@thebiergarten.app";
|
||||
|
||||
// Scenarios run in parallel across feature classes, and this fixture is shared by name.
|
||||
// Serializing provisioning avoids two scenarios racing to INSERT the same username/email,
|
||||
// which would otherwise surface as a raw SQL unique-constraint error instead of a clean 409.
|
||||
private static readonly SemaphoreSlim FixtureProvisioningLock = new(1, 1);
|
||||
|
||||
[Given("I have an existing account")]
|
||||
public void GivenIHaveAnExistingAccount()
|
||||
public async Task GivenIHaveAnExistingAccount()
|
||||
{
|
||||
scenario[TestUserKey] = ("test.user", "password");
|
||||
scenario[TestUserKey] = (FixtureUsername, FixturePassword);
|
||||
|
||||
HttpClient client = GetClient();
|
||||
var registrationData = new
|
||||
{
|
||||
username = FixtureUsername,
|
||||
firstName = "Test",
|
||||
lastName = "User",
|
||||
email = FixtureEmail,
|
||||
dateOfBirth = "1990-01-01",
|
||||
password = FixturePassword,
|
||||
};
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Post, "/api/auth/register")
|
||||
{
|
||||
Content = new StringContent(
|
||||
JsonSerializer.Serialize(registrationData),
|
||||
Encoding.UTF8,
|
||||
"application/json"
|
||||
),
|
||||
};
|
||||
|
||||
// Registration is used (rather than relying on seeded data) so the fixture account is
|
||||
// guaranteed to exist regardless of run order. A 409 means another scenario in this run
|
||||
// already provisioned it, which is equally fine.
|
||||
await FixtureProvisioningLock.WaitAsync();
|
||||
HttpResponseMessage response;
|
||||
try
|
||||
{
|
||||
response = await client.SendAsync(requestMessage);
|
||||
}
|
||||
finally
|
||||
{
|
||||
FixtureProvisioningLock.Release();
|
||||
}
|
||||
|
||||
if ((int)response.StatusCode is not (201 or 409))
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to provision fixture account '{FixtureUsername}': "
|
||||
+ $"{(int)response.StatusCode} {response.StatusCode}"
|
||||
);
|
||||
}
|
||||
|
||||
[Given("I do not have an existing account")]
|
||||
@@ -320,7 +369,7 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
public async Task GivenIAmLoggedIn()
|
||||
{
|
||||
HttpClient client = GetClient();
|
||||
var loginData = new { username = "test.user", password = "password" };
|
||||
var loginData = new { username = FixtureUsername, password = FixturePassword };
|
||||
string body = JsonSerializer.Serialize(loginData);
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Post, "/api/auth/login")
|
||||
|
||||
Reference in New Issue
Block a user