diff --git a/web/backend/API/API.Core/API.Core.csproj b/web/backend/API/API.Core/API.Core.csproj index 6e9825de..4fe70b90 100644 --- a/web/backend/API/API.Core/API.Core.csproj +++ b/web/backend/API/API.Core/API.Core.csproj @@ -8,33 +8,27 @@ - - - - + + + + - + - - - - - - - - - - + + + + + + + + + + diff --git a/web/backend/API/API.Core/Authentication/JwtAuthenticationHandler.cs b/web/backend/API/API.Core/Authentication/JwtAuthenticationHandler.cs index 3833c64d..539c9039 100644 --- a/web/backend/API/API.Core/Authentication/JwtAuthenticationHandler.cs +++ b/web/backend/API/API.Core/Authentication/JwtAuthenticationHandler.cs @@ -44,29 +44,19 @@ public class JwtAuthenticationHandler( { // Use the same access-token secret source as TokenService to avoid mismatched validation. string? secret = Environment.GetEnvironmentVariable("ACCESS_TOKEN_SECRET"); - if (string.IsNullOrWhiteSpace(secret)) secret = configuration["Jwt:SecretKey"]; + if (string.IsNullOrWhiteSpace(secret)) + secret = configuration["Jwt:SecretKey"]; - if (string.IsNullOrWhiteSpace(secret)) return AuthenticateResult.Fail("JWT secret is not configured"); + if (string.IsNullOrWhiteSpace(secret)) + return AuthenticateResult.Fail("JWT secret is not configured"); // Check if Authorization header exists - if ( - !Request.Headers.TryGetValue( - "Authorization", - out StringValues authHeaderValue - ) - ) + if (!Request.Headers.TryGetValue("Authorization", out StringValues authHeaderValue)) return AuthenticateResult.Fail("Authorization header is missing"); string authHeader = authHeaderValue.ToString(); - if ( - !authHeader.StartsWith( - "Bearer ", - StringComparison.OrdinalIgnoreCase - ) - ) - return AuthenticateResult.Fail( - "Invalid authorization header format" - ); + if (!authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + return AuthenticateResult.Fail("Invalid authorization header format"); string token = authHeader.Substring("Bearer ".Length).Trim(); @@ -81,9 +71,7 @@ public class JwtAuthenticationHandler( } catch (Exception ex) { - return AuthenticateResult.Fail( - $"Token validation failed: {ex.Message}" - ); + return AuthenticateResult.Fail($"Token validation failed: {ex.Message}"); } } @@ -97,7 +85,10 @@ public class JwtAuthenticationHandler( Response.ContentType = "application/json"; Response.StatusCode = 401; - ResponseBody response = new() { Message = "Unauthorized: Invalid or missing authentication token" }; + ResponseBody response = new() + { + Message = "Unauthorized: Invalid or missing authentication token", + }; await Response.WriteAsJsonAsync(response); } } @@ -106,6 +97,4 @@ public class JwtAuthenticationHandler( /// Options for the JWT authentication scheme handled by . /// /// No additional options are defined beyond those provided by . -public class JwtAuthenticationOptions : AuthenticationSchemeOptions -{ -} \ No newline at end of file +public class JwtAuthenticationOptions : AuthenticationSchemeOptions { } diff --git a/web/backend/API/API.Core/Controllers/NotFoundController.cs b/web/backend/API/API.Core/Controllers/NotFoundController.cs index 1f3a31e4..35c38903 100644 --- a/web/backend/API/API.Core/Controllers/NotFoundController.cs +++ b/web/backend/API/API.Core/Controllers/NotFoundController.cs @@ -24,4 +24,4 @@ public class NotFoundController : ControllerBase { return NotFound(new { message = "Route not found." }); } -} \ No newline at end of file +} diff --git a/web/backend/API/API.Core/Controllers/ProtectedController.cs b/web/backend/API/API.Core/Controllers/ProtectedController.cs index 4efa192b..02199d4f 100644 --- a/web/backend/API/API.Core/Controllers/ProtectedController.cs +++ b/web/backend/API/API.Core/Controllers/ProtectedController.cs @@ -31,8 +31,8 @@ public class ProtectedController : ControllerBase new ResponseBody { Message = "Protected endpoint accessed successfully", - Payload = new { userId, username } + Payload = new { userId, username }, } ); } -} \ No newline at end of file +} diff --git a/web/backend/API/API.Core/GlobalException.cs b/web/backend/API/API.Core/GlobalException.cs index 78e8325b..ae37dc6a 100644 --- a/web/backend/API/API.Core/GlobalException.cs +++ b/web/backend/API/API.Core/GlobalException.cs @@ -14,8 +14,7 @@ namespace API.Core; /// consistent JSON error responses with appropriate HTTP status codes. /// /// Logger used to record unhandled exceptions before they are translated into a response. -public class GlobalExceptionFilter(ILogger logger) - : IExceptionFilter +public class GlobalExceptionFilter(ILogger logger) : IExceptionFilter { /// /// Logs the exception and sets to an appropriate error response, @@ -84,10 +83,7 @@ public class GlobalExceptionFilter(ILogger logger) case ValidationException fluentValidationException: Dictionary errors = fluentValidationException .Errors.GroupBy(e => e.PropertyName) - .ToDictionary( - g => g.Key, - g => g.Select(e => e.ErrorMessage).ToArray() - ); + .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray()); context.Result = new BadRequestObjectResult( new { message = "Validation failed", errors } @@ -96,41 +92,33 @@ public class GlobalExceptionFilter(ILogger logger) break; case ConflictException ex: - context.Result = new ObjectResult( - new ResponseBody { Message = ex.Message } - ) + context.Result = new ObjectResult(new ResponseBody { Message = ex.Message }) { - StatusCode = 409 + StatusCode = 409, }; context.ExceptionHandled = true; break; case NotFoundException ex: - context.Result = new ObjectResult( - new ResponseBody { Message = ex.Message } - ) + context.Result = new ObjectResult(new ResponseBody { Message = ex.Message }) { - StatusCode = 404 + StatusCode = 404, }; context.ExceptionHandled = true; break; case UnauthorizedException ex: - context.Result = new ObjectResult( - new ResponseBody { Message = ex.Message } - ) + context.Result = new ObjectResult(new ResponseBody { Message = ex.Message }) { - StatusCode = 401 + StatusCode = 401, }; context.ExceptionHandled = true; break; case ForbiddenException ex: - context.Result = new ObjectResult( - new ResponseBody { Message = ex.Message } - ) + context.Result = new ObjectResult(new ResponseBody { Message = ex.Message }) { - StatusCode = 403 + StatusCode = 403, }; context.ExceptionHandled = true; break; @@ -140,33 +128,28 @@ public class GlobalExceptionFilter(ILogger logger) new ResponseBody { Message = "A database error occurred." } ) { - StatusCode = 503 + StatusCode = 503, }; context.ExceptionHandled = true; break; case Domain.Exceptions.ValidationException ex: - context.Result = new ObjectResult( - new ResponseBody { Message = ex.Message } - ) + context.Result = new ObjectResult(new ResponseBody { Message = ex.Message }) { - StatusCode = 400 + StatusCode = 400, }; context.ExceptionHandled = true; break; default: context.Result = new ObjectResult( - new ResponseBody - { - Message = "An unexpected error occurred" - } + new ResponseBody { Message = "An unexpected error occurred" } ) { - StatusCode = 500 + StatusCode = 500, }; context.ExceptionHandled = true; break; } } -} \ No newline at end of file +} diff --git a/web/backend/API/API.Core/Program.cs b/web/backend/API/API.Core/Program.cs index 1bc9b48d..326a1e15 100644 --- a/web/backend/API/API.Core/Program.cs +++ b/web/backend/API/API.Core/Program.cs @@ -17,7 +17,11 @@ using Shared.Application.Behaviors; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); // Global Exception Filter -builder.Services.AddControllers(options => { options.Filters.Add(); }) +builder + .Services.AddControllers(options => + { + options.Filters.Add(); + }) .AddApplicationPart(typeof(BreweryController).Assembly) .AddApplicationPart(typeof(UserController).Assembly) .AddApplicationPart(typeof(AuthController).Assembly); @@ -51,14 +55,12 @@ builder.Services.AddHealthChecks(); // Configure logging for container output builder.Logging.ClearProviders(); builder.Logging.AddConsole(); -if (!builder.Environment.IsProduction()) builder.Logging.AddDebug(); +if (!builder.Environment.IsProduction()) + builder.Logging.AddDebug(); // Configure Dependency Injection ------------------------------------------------------------------------------------- -builder.Services.AddSingleton< - ISqlConnectionFactory, - DefaultSqlConnectionFactory ->(); +builder.Services.AddSingleton(); builder.Services.AddFeaturesBreweries(); builder.Services.AddFeaturesUserManagement(); @@ -75,10 +77,7 @@ builder.Services.AddScoped(); // Configure JWT Authentication builder .Services.AddAuthentication("JWT") - .AddScheme( - "JWT", - options => { } - ); + .AddScheme("JWT", options => { }); builder.Services.AddAuthorization(); @@ -106,4 +105,4 @@ lifetime.ApplicationStopping.Register(() => app.Logger.LogInformation("Application is shutting down gracefully..."); }); -app.Run(); \ No newline at end of file +app.Run(); diff --git a/web/backend/API/API.Specs/API.Specs.csproj b/web/backend/API/API.Specs/API.Specs.csproj index 8aeff0a0..5ae428e8 100644 --- a/web/backend/API/API.Specs/API.Specs.csproj +++ b/web/backend/API/API.Specs/API.Specs.csproj @@ -8,40 +8,37 @@ - - - - - + + + + + - - + + - + - + - + - - - + + + diff --git a/web/backend/API/API.Specs/Mocks/MockEmailDispatcher.cs b/web/backend/API/API.Specs/Mocks/MockEmailDispatcher.cs index e3277d28..656b2bc9 100644 --- a/web/backend/API/API.Specs/Mocks/MockEmailDispatcher.cs +++ b/web/backend/API/API.Specs/Mocks/MockEmailDispatcher.cs @@ -8,11 +8,7 @@ public class MockEmailDispatcher : IEmailDispatcher public List SentResendConfirmationEmails { get; } = new(); - public Task SendRegistrationEmailAsync( - string firstName, - string email, - string confirmationToken - ) + public Task SendRegistrationEmailAsync(string firstName, string email, string confirmationToken) { SentRegistrationEmails.Add( new RegistrationEmail @@ -20,7 +16,7 @@ public class MockEmailDispatcher : IEmailDispatcher FirstName = firstName, Email = email, ConfirmationToken = confirmationToken, - SentAt = DateTime.UtcNow + SentAt = DateTime.UtcNow, } ); @@ -39,7 +35,7 @@ public class MockEmailDispatcher : IEmailDispatcher FirstName = firstName, Email = email, ConfirmationToken = confirmationToken, - SentAt = DateTime.UtcNow + SentAt = DateTime.UtcNow, } ); @@ -67,4 +63,4 @@ public class MockEmailDispatcher : IEmailDispatcher public string ConfirmationToken { get; init; } = string.Empty; public DateTime SentAt { get; init; } } -} \ No newline at end of file +} diff --git a/web/backend/API/API.Specs/Mocks/MockEmailProvider.cs b/web/backend/API/API.Specs/Mocks/MockEmailProvider.cs index ba5cf65e..6d598338 100644 --- a/web/backend/API/API.Specs/Mocks/MockEmailProvider.cs +++ b/web/backend/API/API.Specs/Mocks/MockEmailProvider.cs @@ -10,12 +10,7 @@ public class MockEmailProvider : IEmailProvider { public List SentEmails { get; } = new(); - public Task SendAsync( - string to, - string subject, - string body, - bool isHtml = true - ) + public Task SendAsync(string to, string subject, string body, bool isHtml = true) { SentEmails.Add( new SentEmail @@ -24,19 +19,14 @@ public class MockEmailProvider : IEmailProvider Subject = subject, Body = body, IsHtml = isHtml, - SentAt = DateTime.UtcNow + SentAt = DateTime.UtcNow, } ); return Task.CompletedTask; } - public Task SendAsync( - IEnumerable to, - string subject, - string body, - bool isHtml = true - ) + public Task SendAsync(IEnumerable to, string subject, string body, bool isHtml = true) { SentEmails.Add( new SentEmail @@ -45,7 +35,7 @@ public class MockEmailProvider : IEmailProvider Subject = subject, Body = body, IsHtml = isHtml, - SentAt = DateTime.UtcNow + SentAt = DateTime.UtcNow, } ); @@ -65,4 +55,4 @@ public class MockEmailProvider : IEmailProvider public bool IsHtml { get; init; } public DateTime SentAt { get; init; } } -} \ No newline at end of file +} diff --git a/web/backend/API/API.Specs/Steps/ApiGeneralSteps.cs b/web/backend/API/API.Specs/Steps/ApiGeneralSteps.cs index 65d5940d..c0159449 100644 --- a/web/backend/API/API.Specs/Steps/ApiGeneralSteps.cs +++ b/web/backend/API/API.Specs/Steps/ApiGeneralSteps.cs @@ -15,7 +15,8 @@ public class ApiGeneralSteps(ScenarioContext scenario) private HttpClient GetClient() { - if (scenario.TryGetValue(ClientKey, out HttpClient? client)) return client; + if (scenario.TryGetValue(ClientKey, out HttpClient? client)) + return client; TestApiFactory? factory = scenario.TryGetValue( FactoryKey, @@ -47,11 +48,7 @@ public class ApiGeneralSteps(ScenarioContext scenario) HttpRequestMessage requestMessage = new(new HttpMethod(method), url) { - Content = new StringContent( - jsonBody, - Encoding.UTF8, - "application/json" - ) + Content = new StringContent(jsonBody, Encoding.UTF8, "application/json"), }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -62,16 +59,10 @@ public class ApiGeneralSteps(ScenarioContext scenario) } [When("I send an HTTP request {string} to {string}")] - public async Task WhenISendAnHttpRequestStringToString( - string method, - string url - ) + public async Task WhenISendAnHttpRequestStringToString(string method, string url) { HttpClient client = GetClient(); - HttpRequestMessage requestMessage = new( - new HttpMethod(method), - url - ); + HttpRequestMessage requestMessage = new(new HttpMethod(method), url); HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); @@ -100,19 +91,13 @@ public class ApiGeneralSteps(ScenarioContext scenario) } [Then("the response JSON should have {string} equal {string}")] - public void ThenTheResponseJsonShouldHaveStringEqualString( - string field, - string expected - ) + public void ThenTheResponseJsonShouldHaveStringEqualString(string field, string expected) { scenario .TryGetValue(ResponseKey, out HttpResponseMessage? response) .Should() .BeTrue(); - scenario - .TryGetValue(ResponseBodyKey, out string? responseBody) - .Should() - .BeTrue(); + scenario.TryGetValue(ResponseBodyKey, out string? responseBody).Should().BeTrue(); using JsonDocument doc = JsonDocument.Parse(responseBody!); JsonElement root = doc.RootElement; @@ -125,25 +110,16 @@ public class ApiGeneralSteps(ScenarioContext scenario) "Expected field '{0}' to be present either at the root or inside 'payload'", field ); - payloadElem - .ValueKind.Should() - .Be(JsonValueKind.Object, "payload must be an object"); + payloadElem.ValueKind.Should().Be(JsonValueKind.Object, "payload must be an object"); payloadElem .TryGetProperty(field, out value) .Should() - .BeTrue( - "Expected field '{0}' to be present inside 'payload'", - field - ); + .BeTrue("Expected field '{0}' to be present inside 'payload'", field); } value .ValueKind.Should() - .Be( - JsonValueKind.String, - "Expected field '{0}' to be a string", - field - ); + .Be(JsonValueKind.String, "Expected field '{0}' to be a string", field); value.GetString().Should().Be(expected); } @@ -157,10 +133,7 @@ public class ApiGeneralSteps(ScenarioContext scenario) .TryGetValue(ResponseKey, out HttpResponseMessage? response) .Should() .BeTrue(); - scenario - .TryGetValue(ResponseBodyKey, out string? responseBody) - .Should() - .BeTrue(); + scenario.TryGetValue(ResponseBodyKey, out string? responseBody).Should().BeTrue(); using JsonDocument doc = JsonDocument.Parse(responseBody!); JsonElement root = doc.RootElement; @@ -173,25 +146,16 @@ public class ApiGeneralSteps(ScenarioContext scenario) "Expected field '{0}' to be present either at the root or inside 'payload'", field ); - payloadElem - .ValueKind.Should() - .Be(JsonValueKind.Object, "payload must be an object"); + payloadElem.ValueKind.Should().Be(JsonValueKind.Object, "payload must be an object"); payloadElem .TryGetProperty(field, out value) .Should() - .BeTrue( - "Expected field '{0}' to be present inside 'payload'", - field - ); + .BeTrue("Expected field '{0}' to be present inside 'payload'", field); } value .ValueKind.Should() - .Be( - JsonValueKind.String, - "Expected field '{0}' to be a string", - field - ); + .Be(JsonValueKind.String, "Expected field '{0}' to be a string", field); string? actualValue = value.GetString(); actualValue .Should() @@ -203,4 +167,4 @@ public class ApiGeneralSteps(ScenarioContext scenario) actualValue ); } -} \ No newline at end of file +} diff --git a/web/backend/API/API.Specs/Steps/AuthSteps.cs b/web/backend/API/API.Specs/Steps/AuthSteps.cs index 489d42f4..605859c9 100644 --- a/web/backend/API/API.Specs/Steps/AuthSteps.cs +++ b/web/backend/API/API.Specs/Steps/AuthSteps.cs @@ -21,7 +21,8 @@ public class AuthSteps(ScenarioContext scenario) private HttpClient GetClient() { - if (scenario.TryGetValue(ClientKey, out HttpClient? client)) return client; + if (scenario.TryGetValue(ClientKey, out HttpClient? client)) + return client; TestApiFactory? factory = scenario.TryGetValue( FactoryKey, @@ -39,9 +40,7 @@ public class AuthSteps(ScenarioContext scenario) private static string GetRequiredEnvVar(string name) { return Environment.GetEnvironmentVariable(name) - ?? throw new InvalidOperationException( - $"{name} environment variable is not set" - ); + ?? throw new InvalidOperationException($"{name} environment variable is not set"); } private static string GenerateJwtToken( @@ -57,21 +56,13 @@ public class AuthSteps(ScenarioContext scenario) private static Guid ParseRegisteredUserId(JsonElement root) { - return root - .GetProperty("payload") - .GetProperty("userAccountId") - .GetGuid(); + return root.GetProperty("payload").GetProperty("userAccountId").GetGuid(); } private static string ParseRegisteredUsername(JsonElement root) { - return root - .GetProperty("payload") - .GetProperty("username") - .GetString() - ?? throw new InvalidOperationException( - "username missing from registration payload" - ); + return root.GetProperty("payload").GetProperty("username").GetString() + ?? throw new InvalidOperationException("username missing from registration payload"); } private static string ParseTokenFromPayload( @@ -85,9 +76,7 @@ public class AuthSteps(ScenarioContext scenario) || payload.TryGetProperty(pascalCaseName, out tokenElem) ) return tokenElem.GetString() - ?? throw new InvalidOperationException( - $"{camelCaseName} is null" - ); + ?? throw new InvalidOperationException($"{camelCaseName} is null"); throw new InvalidOperationException( $"Could not find token field '{camelCaseName}' in payload" @@ -113,22 +102,15 @@ public class AuthSteps(ScenarioContext scenario) (string username, string password) = scenario.TryGetValue<( string username, string password - )>(TestUserKey, out (string username, string password) user) + )>(TestUserKey, out (string username, string password) user) ? user : ("test.user", "password"); string body = JsonSerializer.Serialize(new { username, password }); - HttpRequestMessage requestMessage = new( - HttpMethod.Post, - "/api/auth/login" - ) + HttpRequestMessage requestMessage = new(HttpMethod.Post, "/api/auth/login") { - Content = new StringContent( - body, - Encoding.UTF8, - "application/json" - ) + Content = new StringContent(body, Encoding.UTF8, "application/json"), }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -144,16 +126,9 @@ public class AuthSteps(ScenarioContext scenario) HttpClient client = GetClient(); string body = JsonSerializer.Serialize(new { password = "test" }); - HttpRequestMessage requestMessage = new( - HttpMethod.Post, - "/api/auth/login" - ) + HttpRequestMessage requestMessage = new(HttpMethod.Post, "/api/auth/login") { - Content = new StringContent( - body, - Encoding.UTF8, - "application/json" - ) + Content = new StringContent(body, Encoding.UTF8, "application/json"), }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -169,16 +144,9 @@ public class AuthSteps(ScenarioContext scenario) HttpClient client = GetClient(); string body = JsonSerializer.Serialize(new { username = "test" }); - HttpRequestMessage requestMessage = new( - HttpMethod.Post, - "/api/auth/login" - ) + HttpRequestMessage requestMessage = new(HttpMethod.Post, "/api/auth/login") { - Content = new StringContent( - body, - Encoding.UTF8, - "application/json" - ) + Content = new StringContent(body, Encoding.UTF8, "application/json"), }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -192,16 +160,9 @@ public class AuthSteps(ScenarioContext scenario) public async Task WhenISubmitALoginRequestWithBothUsernameAndPasswordMissing() { HttpClient client = GetClient(); - HttpRequestMessage requestMessage = new( - HttpMethod.Post, - "/api/auth/login" - ) + HttpRequestMessage requestMessage = new(HttpMethod.Post, "/api/auth/login") { - Content = new StringContent( - "{}", - Encoding.UTF8, - "application/json" - ) + Content = new StringContent("{}", Encoding.UTF8, "application/json"), }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -218,10 +179,7 @@ public class AuthSteps(ScenarioContext scenario) .TryGetValue(ResponseKey, out HttpResponseMessage? response) .Should() .BeTrue(); - scenario - .TryGetValue(ResponseBodyKey, out string? responseBody) - .Should() - .BeTrue(); + scenario.TryGetValue(ResponseBodyKey, out string? responseBody).Should().BeTrue(); JsonDocument doc = JsonDocument.Parse(responseBody!); JsonElement root = doc.RootElement; @@ -236,11 +194,7 @@ public class AuthSteps(ScenarioContext scenario) payloadElem.TryGetProperty("accessToken", out tokenElem) || payloadElem.TryGetProperty("AccessToken", out tokenElem); - hasToken - .Should() - .BeTrue( - "Expected an access token either at the root or inside 'payload'" - ); + hasToken.Should().BeTrue("Expected an access token either at the root or inside 'payload'"); string? token = tokenElem.GetString(); token.Should().NotBeNullOrEmpty(); @@ -251,16 +205,9 @@ public class AuthSteps(ScenarioContext scenario) { HttpClient client = GetClient(); // testing GET - HttpRequestMessage requestMessage = new( - HttpMethod.Get, - "/api/auth/login" - ) + HttpRequestMessage requestMessage = new(HttpMethod.Get, "/api/auth/login") { - Content = new StringContent( - "{}", - Encoding.UTF8, - "application/json" - ) + Content = new StringContent("{}", Encoding.UTF8, "application/json"), }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -282,7 +229,8 @@ public class AuthSteps(ScenarioContext scenario) string email = row["Email"] ?? ""; string dateOfBirth = row["DateOfBirth"] ?? ""; - if (dateOfBirth == "{underage_date}") dateOfBirth = DateTime.UtcNow.AddYears(-18).ToString("yyyy-MM-dd"); + if (dateOfBirth == "{underage_date}") + dateOfBirth = DateTime.UtcNow.AddYears(-18).ToString("yyyy-MM-dd"); // Keep default registration fixture values unique across repeated runs. if (email == "newuser@example.com") @@ -290,7 +238,8 @@ public class AuthSteps(ScenarioContext scenario) string suffix = Guid.NewGuid().ToString("N")[..8]; email = $"newuser-{suffix}@example.com"; - if (username == "newuser") username = $"newuser-{suffix}"; + if (username == "newuser") + username = $"newuser-{suffix}"; } string? password = row["Password"]; @@ -302,21 +251,14 @@ public class AuthSteps(ScenarioContext scenario) lastName, email, dateOfBirth, - password + password, }; string body = JsonSerializer.Serialize(registrationData); - HttpRequestMessage requestMessage = new( - HttpMethod.Post, - "/api/auth/register" - ) + HttpRequestMessage requestMessage = new(HttpMethod.Post, "/api/auth/register") { - Content = new StringContent( - body, - Encoding.UTF8, - "application/json" - ) + Content = new StringContent(body, Encoding.UTF8, "application/json"), }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -330,16 +272,9 @@ public class AuthSteps(ScenarioContext scenario) public async Task WhenISubmitARegistrationRequestUsingAGetRequest() { HttpClient client = GetClient(); - HttpRequestMessage requestMessage = new( - HttpMethod.Get, - "/api/auth/register" - ) + HttpRequestMessage requestMessage = new(HttpMethod.Get, "/api/auth/register") { - Content = new StringContent( - "{}", - Encoding.UTF8, - "application/json" - ) + Content = new StringContent("{}", Encoding.UTF8, "application/json"), }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -361,20 +296,13 @@ public class AuthSteps(ScenarioContext scenario) lastName = "User", email = $"newuser-{suffix}@example.com", dateOfBirth = "1990-01-01", - password = "Password1!" + password = "Password1!", }; string body = JsonSerializer.Serialize(registrationData); - HttpRequestMessage requestMessage = new( - HttpMethod.Post, - "/api/auth/register" - ) + HttpRequestMessage requestMessage = new(HttpMethod.Post, "/api/auth/register") { - Content = new StringContent( - body, - Encoding.UTF8, - "application/json" - ) + Content = new StringContent(body, Encoding.UTF8, "application/json"), }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -395,16 +323,9 @@ public class AuthSteps(ScenarioContext scenario) var loginData = new { username = "test.user", password = "password" }; string body = JsonSerializer.Serialize(loginData); - HttpRequestMessage requestMessage = new( - HttpMethod.Post, - "/api/auth/login" - ) + HttpRequestMessage requestMessage = new(HttpMethod.Post, "/api/auth/login") { - Content = new StringContent( - body, - Encoding.UTF8, - "application/json" - ) + Content = new StringContent(body, Encoding.UTF8, "application/json"), }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -445,17 +366,10 @@ public class AuthSteps(ScenarioContext scenario) { Guid userId = scenario.TryGetValue(RegisteredUserIdKey, out Guid id) ? id - : throw new InvalidOperationException( - "registered user ID not found in scenario" - ); - string? username = scenario.TryGetValue( - RegisteredUsernameKey, - out string? user - ) + : throw new InvalidOperationException("registered user ID not found in scenario"); + string? username = scenario.TryGetValue(RegisteredUsernameKey, out string? user) ? user - : throw new InvalidOperationException( - "registered username not found in scenario" - ); + : throw new InvalidOperationException("registered username not found in scenario"); string secret = GetRequiredEnvVar("ACCESS_TOKEN_SECRET"); scenario["accessToken"] = GenerateJwtToken( @@ -471,17 +385,10 @@ public class AuthSteps(ScenarioContext scenario) { Guid userId = scenario.TryGetValue(RegisteredUserIdKey, out Guid id) ? id - : throw new InvalidOperationException( - "registered user ID not found in scenario" - ); - string? username = scenario.TryGetValue( - RegisteredUsernameKey, - out string? user - ) + : throw new InvalidOperationException("registered user ID not found in scenario"); + string? username = scenario.TryGetValue(RegisteredUsernameKey, out string? user) ? user - : throw new InvalidOperationException( - "registered username not found in scenario" - ); + : throw new InvalidOperationException("registered username not found in scenario"); string secret = GetRequiredEnvVar("CONFIRMATION_TOKEN_SECRET"); scenario["confirmationToken"] = GenerateJwtToken( @@ -497,17 +404,10 @@ public class AuthSteps(ScenarioContext scenario) { Guid userId = scenario.TryGetValue(RegisteredUserIdKey, out Guid id) ? id - : throw new InvalidOperationException( - "registered user ID not found in scenario" - ); - string? username = scenario.TryGetValue( - RegisteredUsernameKey, - out string? user - ) + : throw new InvalidOperationException("registered user ID not found in scenario"); + string? username = scenario.TryGetValue(RegisteredUsernameKey, out string? user) ? user - : throw new InvalidOperationException( - "registered username not found in scenario" - ); + : throw new InvalidOperationException("registered username not found in scenario"); string secret = GetRequiredEnvVar("CONFIRMATION_TOKEN_SECRET"); scenario["confirmationToken"] = GenerateJwtToken( @@ -523,20 +423,12 @@ public class AuthSteps(ScenarioContext scenario) { Guid userId = scenario.TryGetValue(RegisteredUserIdKey, out Guid id) ? id - : throw new InvalidOperationException( - "registered user ID not found in scenario" - ); - string? username = scenario.TryGetValue( - RegisteredUsernameKey, - out string? user - ) + : throw new InvalidOperationException("registered user ID not found in scenario"); + string? username = scenario.TryGetValue(RegisteredUsernameKey, out string? user) ? user - : throw new InvalidOperationException( - "registered username not found in scenario" - ); + : throw new InvalidOperationException("registered username not found in scenario"); - const string wrongSecret = - "wrong-confirmation-secret-that-is-very-long-1234567890"; + const string wrongSecret = "wrong-confirmation-secret-that-is-very-long-1234567890"; scenario["confirmationToken"] = GenerateJwtToken( userId, username, @@ -545,9 +437,7 @@ public class AuthSteps(ScenarioContext scenario) ); } - [When( - "I submit a request to a protected endpoint with a valid access token" - )] + [When("I submit a request to a protected endpoint with a valid access token")] public async Task WhenISubmitARequestToAProtectedEndpointWithAValidAccessToken() { HttpClient client = GetClient(); @@ -555,12 +445,9 @@ public class AuthSteps(ScenarioContext scenario) ? t : "invalid-token"; - HttpRequestMessage requestMessage = new( - HttpMethod.Get, - "/api/protected" - ) + HttpRequestMessage requestMessage = new(HttpMethod.Get, "/api/protected") { - Headers = { { "Authorization", $"Bearer {token}" } } + Headers = { { "Authorization", $"Bearer {token}" } }, }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -569,18 +456,13 @@ public class AuthSteps(ScenarioContext scenario) scenario[ResponseBodyKey] = responseBody; } - [When( - "I submit a request to a protected endpoint with an invalid access token" - )] + [When("I submit a request to a protected endpoint with an invalid access token")] public async Task WhenISubmitARequestToAProtectedEndpointWithAnInvalidAccessToken() { HttpClient client = GetClient(); - HttpRequestMessage requestMessage = new( - HttpMethod.Get, - "/api/protected" - ) + HttpRequestMessage requestMessage = new(HttpMethod.Get, "/api/protected") { - Headers = { { "Authorization", "Bearer invalid-token-format" } } + Headers = { { "Authorization", "Bearer invalid-token-format" } }, }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -673,16 +555,9 @@ public class AuthSteps(ScenarioContext scenario) : "valid-refresh-token"; string body = JsonSerializer.Serialize(new { refreshToken = token }); - HttpRequestMessage requestMessage = new( - HttpMethod.Post, - "/api/auth/refresh" - ) + HttpRequestMessage requestMessage = new(HttpMethod.Post, "/api/auth/refresh") { - Content = new StringContent( - body, - Encoding.UTF8, - "application/json" - ) + Content = new StringContent(body, Encoding.UTF8, "application/json"), }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -695,20 +570,11 @@ public class AuthSteps(ScenarioContext scenario) public async Task WhenISubmitARefreshTokenRequestWithAnInvalidRefreshToken() { HttpClient client = GetClient(); - string body = JsonSerializer.Serialize( - new { refreshToken = "invalid-refresh-token" } - ); + string body = JsonSerializer.Serialize(new { refreshToken = "invalid-refresh-token" }); - HttpRequestMessage requestMessage = new( - HttpMethod.Post, - "/api/auth/refresh" - ) + HttpRequestMessage requestMessage = new(HttpMethod.Post, "/api/auth/refresh") { - Content = new StringContent( - body, - Encoding.UTF8, - "application/json" - ) + Content = new StringContent(body, Encoding.UTF8, "application/json"), }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -722,20 +588,11 @@ public class AuthSteps(ScenarioContext scenario) { HttpClient client = GetClient(); // Use an expired token - string body = JsonSerializer.Serialize( - new { refreshToken = "expired-refresh-token" } - ); + string body = JsonSerializer.Serialize(new { refreshToken = "expired-refresh-token" }); - HttpRequestMessage requestMessage = new( - HttpMethod.Post, - "/api/auth/refresh" - ) + HttpRequestMessage requestMessage = new(HttpMethod.Post, "/api/auth/refresh") { - Content = new StringContent( - body, - Encoding.UTF8, - "application/json" - ) + Content = new StringContent(body, Encoding.UTF8, "application/json"), }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -750,16 +607,9 @@ public class AuthSteps(ScenarioContext scenario) HttpClient client = GetClient(); string body = JsonSerializer.Serialize(new { }); - HttpRequestMessage requestMessage = new( - HttpMethod.Post, - "/api/auth/refresh" - ) + HttpRequestMessage requestMessage = new(HttpMethod.Post, "/api/auth/refresh") { - Content = new StringContent( - body, - Encoding.UTF8, - "application/json" - ) + Content = new StringContent(body, Encoding.UTF8, "application/json"), }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -772,16 +622,9 @@ public class AuthSteps(ScenarioContext scenario) public async Task WhenISubmitARefreshTokenRequestUsingAGETRequest() { HttpClient client = GetClient(); - HttpRequestMessage requestMessage = new( - HttpMethod.Get, - "/api/auth/refresh" - ) + HttpRequestMessage requestMessage = new(HttpMethod.Get, "/api/auth/refresh") { - Content = new StringContent( - "{}", - Encoding.UTF8, - "application/json" - ) + Content = new StringContent("{}", Encoding.UTF8, "application/json"), }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -795,10 +638,7 @@ public class AuthSteps(ScenarioContext scenario) public async Task WhenISubmitARequestToAProtectedEndpointWithoutAnAccessToken() { HttpClient client = GetClient(); - HttpRequestMessage requestMessage = new( - HttpMethod.Get, - "/api/protected" - ); + HttpRequestMessage requestMessage = new(HttpMethod.Get, "/api/protected"); HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); @@ -830,12 +670,9 @@ public class AuthSteps(ScenarioContext scenario) ? t : "expired-token"; - HttpRequestMessage requestMessage = new( - HttpMethod.Get, - "/api/protected" - ) + HttpRequestMessage requestMessage = new(HttpMethod.Get, "/api/protected") { - Headers = { { "Authorization", $"Bearer {token}" } } + Headers = { { "Authorization", $"Bearer {token}" } }, }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -852,12 +689,9 @@ public class AuthSteps(ScenarioContext scenario) ? t : "tampered-token"; - HttpRequestMessage requestMessage = new( - HttpMethod.Get, - "/api/protected" - ) + HttpRequestMessage requestMessage = new(HttpMethod.Get, "/api/protected") { - Headers = { { "Authorization", $"Bearer {token}" } } + Headers = { { "Authorization", $"Bearer {token}" } }, }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -876,12 +710,9 @@ public class AuthSteps(ScenarioContext scenario) ? t : "refresh-token"; - HttpRequestMessage requestMessage = new( - HttpMethod.Get, - "/api/protected" - ) + HttpRequestMessage requestMessage = new(HttpMethod.Get, "/api/protected") { - Headers = { { "Authorization", $"Bearer {token}" } } + Headers = { { "Authorization", $"Bearer {token}" } }, }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -991,12 +822,9 @@ public class AuthSteps(ScenarioContext scenario) ? t : "confirmation-token"; - HttpRequestMessage requestMessage = new( - HttpMethod.Get, - "/api/protected" - ) + HttpRequestMessage requestMessage = new(HttpMethod.Get, "/api/protected") { - Headers = { { "Authorization", $"Bearer {token}" } } + Headers = { { "Authorization", $"Bearer {token}" } }, }; HttpResponseMessage response = await client.SendAsync(requestMessage); @@ -1049,54 +877,30 @@ public class AuthSteps(ScenarioContext scenario) [Then("the response JSON should have a new access token")] public void ThenTheResponseJsonShouldHaveANewAccessToken() { - scenario - .TryGetValue(ResponseBodyKey, out string? responseBody) - .Should() - .BeTrue(); + scenario.TryGetValue(ResponseBodyKey, out string? responseBody).Should().BeTrue(); using JsonDocument doc = JsonDocument.Parse(responseBody!); JsonElement payload = doc.RootElement.GetProperty("payload"); - string accessToken = ParseTokenFromPayload( - payload, - "accessToken", - "AccessToken" - ); + string accessToken = ParseTokenFromPayload(payload, "accessToken", "AccessToken"); accessToken.Should().NotBeNullOrWhiteSpace(); - if ( - scenario.TryGetValue( - PreviousAccessTokenKey, - out string? previousAccessToken - ) - ) + if (scenario.TryGetValue(PreviousAccessTokenKey, out string? previousAccessToken)) accessToken.Should().NotBe(previousAccessToken); } [Then("the response JSON should have a new refresh token")] public void ThenTheResponseJsonShouldHaveANewRefreshToken() { - scenario - .TryGetValue(ResponseBodyKey, out string? responseBody) - .Should() - .BeTrue(); + scenario.TryGetValue(ResponseBodyKey, out string? responseBody).Should().BeTrue(); using JsonDocument doc = JsonDocument.Parse(responseBody!); JsonElement payload = doc.RootElement.GetProperty("payload"); - string refreshToken = ParseTokenFromPayload( - payload, - "refreshToken", - "RefreshToken" - ); + string refreshToken = ParseTokenFromPayload(payload, "refreshToken", "RefreshToken"); refreshToken.Should().NotBeNullOrWhiteSpace(); - if ( - scenario.TryGetValue( - PreviousRefreshTokenKey, - out string? previousRefreshToken - ) - ) + if (scenario.TryGetValue(PreviousRefreshTokenKey, out string? previousRefreshToken)) refreshToken.Should().NotBe(previousRefreshToken); } @@ -1186,4 +990,4 @@ public class AuthSteps(ScenarioContext scenario) scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } -} \ No newline at end of file +} diff --git a/web/backend/API/API.Specs/TestApiFactory.cs b/web/backend/API/API.Specs/TestApiFactory.cs index b01dc65e..2c55a164 100644 --- a/web/backend/API/API.Specs/TestApiFactory.cs +++ b/web/backend/API/API.Specs/TestApiFactory.cs @@ -20,7 +20,8 @@ public class TestApiFactory : WebApplicationFactory d.ServiceType == typeof(IEmailProvider) ); - if (emailProviderDescriptor != null) services.Remove(emailProviderDescriptor); + if (emailProviderDescriptor != null) + services.Remove(emailProviderDescriptor); services.AddScoped(); @@ -29,9 +30,10 @@ public class TestApiFactory : WebApplicationFactory d.ServiceType == typeof(IEmailDispatcher) ); - if (emailDispatcherDescriptor != null) services.Remove(emailDispatcherDescriptor); + if (emailDispatcherDescriptor != null) + services.Remove(emailDispatcherDescriptor); services.AddScoped(); }); } -} \ No newline at end of file +} diff --git a/web/backend/Database/Database.Migrations/Database.Migrations.csproj b/web/backend/Database/Database.Migrations/Database.Migrations.csproj index fced2a14..0ec05c6c 100644 --- a/web/backend/Database/Database.Migrations/Database.Migrations.csproj +++ b/web/backend/Database/Database.Migrations/Database.Migrations.csproj @@ -9,11 +9,11 @@ - - + + - + diff --git a/web/backend/Database/Database.Migrations/Program.cs b/web/backend/Database/Database.Migrations/Program.cs index b4afe9c6..672648c8 100644 --- a/web/backend/Database/Database.Migrations/Program.cs +++ b/web/backend/Database/Database.Migrations/Program.cs @@ -35,21 +35,25 @@ public static class Program /// private static string BuildConnectionString(string? databaseName = null) { - string server = Environment.GetEnvironmentVariable("DB_SERVER") - ?? throw new InvalidOperationException("DB_SERVER environment variable is not set"); + string server = + Environment.GetEnvironmentVariable("DB_SERVER") + ?? throw new InvalidOperationException("DB_SERVER environment variable is not set"); - string dbName = databaseName - ?? Environment.GetEnvironmentVariable("DB_NAME") - ?? throw new InvalidOperationException("DB_NAME environment variable is not set"); + string dbName = + databaseName + ?? Environment.GetEnvironmentVariable("DB_NAME") + ?? throw new InvalidOperationException("DB_NAME environment variable is not set"); - string user = Environment.GetEnvironmentVariable("DB_USER") - ?? throw new InvalidOperationException("DB_USER environment variable is not set"); + string user = + Environment.GetEnvironmentVariable("DB_USER") + ?? throw new InvalidOperationException("DB_USER environment variable is not set"); - string password = Environment.GetEnvironmentVariable("DB_PASSWORD") - ?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set"); + string password = + Environment.GetEnvironmentVariable("DB_PASSWORD") + ?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set"); - string trustServerCertificate = Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE") - ?? "True"; + string trustServerCertificate = + Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE") ?? "True"; SqlConnectionStringBuilder builder = new() { @@ -58,7 +62,7 @@ public static class Program UserID = user, Password = password, TrustServerCertificate = bool.Parse(trustServerCertificate), - Encrypt = true + Encrypt = true, }; return builder.ConnectionString; @@ -99,9 +103,10 @@ public static class Program // First, set the database to single user mode to close all connections SqlCommand setModeCommand = new( - "IF EXISTS (SELECT 1 FROM sys.databases WHERE name = 'Biergarten') " + - "ALTER DATABASE [Biergarten] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;", - myConn); + "IF EXISTS (SELECT 1 FROM sys.databases WHERE name = 'Biergarten') " + + "ALTER DATABASE [Biergarten] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;", + myConn + ); try { setModeCommand.ExecuteNonQuery(); @@ -132,7 +137,8 @@ public static class Program } finally { - if (myConn.State == ConnectionState.Open) myConn.Close(); + if (myConn.State == ConnectionState.Open) + myConn.Close(); } return true; @@ -149,9 +155,9 @@ public static class Program SqlConnection myConn = new(masterConnectionString); const string str = """ - IF NOT EXISTS (SELECT 1 FROM sys.databases WHERE name = 'Biergarten') - CREATE DATABASE [Biergarten] - """; + IF NOT EXISTS (SELECT 1 FROM sys.databases WHERE name = 'Biergarten') + CREATE DATABASE [Biergarten] + """; SqlCommand myCommand = new(str, myConn); try @@ -166,7 +172,8 @@ public static class Program } finally { - if (myConn.State == ConnectionState.Open) myConn.Close(); + if (myConn.State == ConnectionState.Open) + myConn.Close(); } return true; @@ -211,4 +218,4 @@ public static class Program return 1; } } -} \ No newline at end of file +} diff --git a/web/backend/Database/Database.Seed/Database.Seed.csproj b/web/backend/Database/Database.Seed/Database.Seed.csproj index 45c69e95..9d11ce11 100644 --- a/web/backend/Database/Database.Seed/Database.Seed.csproj +++ b/web/backend/Database/Database.Seed/Database.Seed.csproj @@ -8,16 +8,13 @@ - - - - + + + + - + diff --git a/web/backend/Database/Database.Seed/ISeeder.cs b/web/backend/Database/Database.Seed/ISeeder.cs index 94e1a83f..7c089389 100644 --- a/web/backend/Database/Database.Seed/ISeeder.cs +++ b/web/backend/Database/Database.Seed/ISeeder.cs @@ -16,4 +16,4 @@ internal interface ISeeder /// An open connection to the target database. /// A task that completes when seeding is finished. Task SeedAsync(SqlConnection connection); -} \ No newline at end of file +} diff --git a/web/backend/Database/Database.Seed/LocationSeeder.cs b/web/backend/Database/Database.Seed/LocationSeeder.cs index b3b971c2..e3ba5c29 100644 --- a/web/backend/Database/Database.Seed/LocationSeeder.cs +++ b/web/backend/Database/Database.Seed/LocationSeeder.cs @@ -15,24 +15,22 @@ namespace Database.Seed; internal class LocationSeeder : ISeeder { /// The set of countries to seed, identified by name and ISO 3166-1 code. - private static readonly IReadOnlyList<( - string CountryName, - string CountryCode - )> Countries = + private static readonly IReadOnlyList<(string CountryName, string CountryCode)> Countries = [ ("Canada", "CA"), ("Mexico", "MX"), - ("United States", "US") + ("United States", "US"), ]; /// /// The set of states/provinces to seed, each identified by name, ISO 3166-2 code, /// and the ISO 3166-1 code of its parent country. /// - private static IReadOnlyList<(string StateProvinceName, string StateProvinceCode, string CountryCode)> States - { - get; - } = + private static IReadOnlyList<( + string StateProvinceName, + string StateProvinceCode, + string CountryCode + )> States { get; } = [ ("Alabama", "US-AL", "US"), ("Alaska", "US-AK", "US"), @@ -134,7 +132,7 @@ internal class LocationSeeder : ISeeder ("Veracruz de Ignacio de la Llave", "MX-VER", "MX"), ("Yucatán", "MX-YUC", "MX"), ("Zacatecas", "MX-ZAC", "MX"), - ("Ciudad de México", "MX-CMX", "MX") + ("Ciudad de México", "MX-CMX", "MX"), ]; /// @@ -255,7 +253,7 @@ internal class LocationSeeder : ISeeder ("MX-COA", "Saltillo"), ("MX-BCS", "La Paz"), ("MX-NAY", "Tepic"), - ("MX-ZAC", "Zacatecas") + ("MX-ZAC", "Zacatecas"), ]; /// @@ -269,9 +267,7 @@ internal class LocationSeeder : ISeeder foreach ((string countryName, string countryCode) in Countries) await CreateCountryAsync(connection, countryName, countryCode); - foreach ( - (string stateProvinceName, string stateProvinceCode, string countryCode) in States - ) + foreach ((string stateProvinceName, string stateProvinceCode, string countryCode) in States) await CreateStateProvinceAsync( connection, stateProvinceName, @@ -294,10 +290,7 @@ internal class LocationSeeder : ISeeder string countryCode ) { - await using SqlCommand command = new( - "dbo.USP_CreateCountry", - connection - ); + await using SqlCommand command = new("dbo.USP_CreateCountry", connection); command.CommandType = CommandType.StoredProcedure; command.Parameters.AddWithValue("@CountryName", countryName); command.Parameters.AddWithValue("@ISO3166_1", countryCode); @@ -318,15 +311,9 @@ internal class LocationSeeder : ISeeder string countryCode ) { - await using SqlCommand command = new( - "dbo.USP_CreateStateProvince", - connection - ); + await using SqlCommand command = new("dbo.USP_CreateStateProvince", connection); command.CommandType = CommandType.StoredProcedure; - command.Parameters.AddWithValue( - "@StateProvinceName", - stateProvinceName - ); + command.Parameters.AddWithValue("@StateProvinceName", stateProvinceName); command.Parameters.AddWithValue("@ISO3166_2", stateProvinceCode); command.Parameters.AddWithValue("@CountryCode", countryCode); @@ -344,17 +331,11 @@ internal class LocationSeeder : ISeeder string stateProvinceCode ) { - await using SqlCommand command = new( - "dbo.USP_CreateCity", - connection - ); + await using SqlCommand command = new("dbo.USP_CreateCity", connection); command.CommandType = CommandType.StoredProcedure; command.Parameters.AddWithValue("@CityName", cityName); - command.Parameters.AddWithValue( - "@StateProvinceCode", - stateProvinceCode - ); + command.Parameters.AddWithValue("@StateProvinceCode", stateProvinceCode); await command.ExecuteNonQueryAsync(); } -} \ No newline at end of file +} diff --git a/web/backend/Database/Database.Seed/Program.cs b/web/backend/Database/Database.Seed/Program.cs index 47d2cd59..fa048de0 100644 --- a/web/backend/Database/Database.Seed/Program.cs +++ b/web/backend/Database/Database.Seed/Program.cs @@ -17,20 +17,24 @@ using Microsoft.Data.SqlClient; /// string BuildConnectionString() { - string server = Environment.GetEnvironmentVariable("DB_SERVER") - ?? throw new InvalidOperationException("DB_SERVER environment variable is not set"); + string server = + Environment.GetEnvironmentVariable("DB_SERVER") + ?? throw new InvalidOperationException("DB_SERVER environment variable is not set"); - string dbName = Environment.GetEnvironmentVariable("DB_NAME") - ?? throw new InvalidOperationException("DB_NAME environment variable is not set"); + string dbName = + Environment.GetEnvironmentVariable("DB_NAME") + ?? throw new InvalidOperationException("DB_NAME environment variable is not set"); - string user = Environment.GetEnvironmentVariable("DB_USER") - ?? throw new InvalidOperationException("DB_USER environment variable is not set"); + string user = + Environment.GetEnvironmentVariable("DB_USER") + ?? throw new InvalidOperationException("DB_USER environment variable is not set"); - string password = Environment.GetEnvironmentVariable("DB_PASSWORD") - ?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set"); + string password = + Environment.GetEnvironmentVariable("DB_PASSWORD") + ?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set"); - string trustServerCertificate = Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE") - ?? "True"; + string trustServerCertificate = + Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE") ?? "True"; SqlConnectionStringBuilder builder = new() { @@ -39,13 +43,12 @@ string BuildConnectionString() UserID = user, Password = password, TrustServerCertificate = bool.Parse(trustServerCertificate), - Encrypt = true + Encrypt = true, }; return builder.ConnectionString; } - try { string connectionString = BuildConnectionString(); @@ -74,17 +77,14 @@ try connection = null; } - if (connection == null) throw new Exception($"Failed to connect to database after {maxRetries} attempts."); + if (connection == null) + throw new Exception($"Failed to connect to database after {maxRetries} attempts."); Console.WriteLine("Starting seeding..."); using (connection) { - ISeeder[] seeders = - [ - new LocationSeeder(), - new UserSeeder() - ]; + ISeeder[] seeders = [new LocationSeeder(), new UserSeeder()]; foreach (ISeeder seeder in seeders) { @@ -103,4 +103,4 @@ catch (Exception ex) Console.Error.WriteLine("Seed failed:"); Console.Error.WriteLine(ex); return 1; -} \ No newline at end of file +} diff --git a/web/backend/Database/Database.Seed/UserSeeder.cs b/web/backend/Database/Database.Seed/UserSeeder.cs index 32b2c984..5a3a312d 100644 --- a/web/backend/Database/Database.Seed/UserSeeder.cs +++ b/web/backend/Database/Database.Seed/UserSeeder.cs @@ -19,10 +19,7 @@ namespace Database.Seed; internal class UserSeeder : ISeeder { /// The first/last name pairs used to generate seed user accounts. - private static readonly IReadOnlyList<( - string FirstName, - string LastName - )> SeedNames = + private static readonly IReadOnlyList<(string FirstName, string LastName)> SeedNames = [ ("Aarya", "Mathews"), ("Aiden", "Wells"), @@ -123,7 +120,7 @@ internal class UserSeeder : ISeeder ("Zara", "Wilkinson"), ("Zaria", "Gibson"), ("Zion", "Watkins"), - ("Zoie", "Armstrong") + ("Zoie", "Armstrong"), ]; /// @@ -168,14 +165,9 @@ internal class UserSeeder : ISeeder DateTime dob = GenerateDateOfBirth(rng); // generate a password and hash it - string pwd = generator.Generate( - 64, - 10, - 10 - ); + string pwd = generator.Generate(64, 10, 10); string hash = GeneratePasswordHash(pwd); - // register the user (creates account + credential) Guid id = await RegisterUserAsync( connection, @@ -189,9 +181,9 @@ internal class UserSeeder : ISeeder createdUsers++; createdCredentials++; - // add user verification - if (await HasUserVerificationAsync(connection, id)) continue; + if (await HasUserVerificationAsync(connection, id)) + continue; await AddUserVerificationAsync(connection, id); createdVerifications++; @@ -227,7 +219,6 @@ internal class UserSeeder : ISeeder await using SqlCommand command = new("dbo.USP_RegisterUser", connection); command.CommandType = CommandType.StoredProcedure; - command.Parameters.Add("@Username", SqlDbType.VarChar, 64).Value = username; command.Parameters.Add("@FirstName", SqlDbType.NVarChar, 128).Value = firstName; command.Parameters.Add("@LastName", SqlDbType.NVarChar, 128).Value = lastName; @@ -237,7 +228,6 @@ internal class UserSeeder : ISeeder object? result = await command.ExecuteScalarAsync(); - return (Guid)result!; } @@ -257,7 +247,7 @@ internal class UserSeeder : ISeeder Salt = salt, DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1), MemorySize = 65536, - Iterations = 4 + Iterations = 4, }; byte[] hash = argon2.GetBytes(32); @@ -274,10 +264,10 @@ internal class UserSeeder : ISeeder ) { const string sql = """ - SELECT 1 - FROM dbo.UserVerification - WHERE UserAccountId = @UserAccountId; - """; + SELECT 1 + FROM dbo.UserVerification + WHERE UserAccountId = @UserAccountId; + """; await using SqlCommand command = new(sql, connection); command.Parameters.AddWithValue("@UserAccountId", userAccountId); object? result = await command.ExecuteScalarAsync(); @@ -288,15 +278,9 @@ internal class UserSeeder : ISeeder /// An open connection to the target database. /// The identifier of the user account to verify. /// A task that completes when the verification record has been created. - private static async Task AddUserVerificationAsync( - SqlConnection connection, - Guid userAccountId - ) + private static async Task AddUserVerificationAsync(SqlConnection connection, Guid userAccountId) { - await using SqlCommand command = new( - "dbo.USP_CreateUserVerification", - connection - ); + await using SqlCommand command = new("dbo.USP_CreateUserVerification", connection); command.CommandType = CommandType.StoredProcedure; command.Parameters.AddWithValue("@UserAccountID_", userAccountId); @@ -316,4 +300,4 @@ internal class UserSeeder : ISeeder int offsetDays = random.Next(0, 365); return baseDate.AddDays(-offsetDays); } -} \ No newline at end of file +} diff --git a/web/backend/Domain/Domain.Entities/Entities/BreweryPost.cs b/web/backend/Domain/Domain.Entities/Entities/BreweryPost.cs index 0594a9b0..9bac7e6a 100644 --- a/web/backend/Domain/Domain.Entities/Entities/BreweryPost.cs +++ b/web/backend/Domain/Domain.Entities/Entities/BreweryPost.cs @@ -47,4 +47,4 @@ public class BreweryPost /// navigation property. /// public BreweryPostLocation? Location { get; set; } -} \ No newline at end of file +} diff --git a/web/backend/Domain/Domain.Entities/Entities/BreweryPostLocation.cs b/web/backend/Domain/Domain.Entities/Entities/BreweryPostLocation.cs index c59917d3..8de9e6a6 100644 --- a/web/backend/Domain/Domain.Entities/Entities/BreweryPostLocation.cs +++ b/web/backend/Domain/Domain.Entities/Entities/BreweryPostLocation.cs @@ -7,46 +7,46 @@ namespace Domain.Entities; /// public class BreweryPostLocation { - /// - /// Primary key identifying this location. Maps to BreweryPostLocationID. - /// - public Guid BreweryPostLocationId { get; set; } + /// + /// Primary key identifying this location. Maps to BreweryPostLocationID. + /// + public Guid BreweryPostLocationId { get; set; } - /// - /// Foreign key referencing the owning . Maps to BreweryPostID; unique, enforcing a - /// one-to-one relationship. - /// - public Guid BreweryPostId { get; set; } + /// + /// Foreign key referencing the owning . Maps to BreweryPostID; unique, enforcing a + /// one-to-one relationship. + /// + public Guid BreweryPostId { get; set; } - /// - /// The primary street address line. - /// - public string AddressLine1 { get; set; } = string.Empty; + /// + /// The primary street address line. + /// + public string AddressLine1 { get; set; } = string.Empty; - /// - /// An optional secondary address line (e.g., suite or unit number). - /// - public string? AddressLine2 { get; set; } + /// + /// An optional secondary address line (e.g., suite or unit number). + /// + public string? AddressLine2 { get; set; } - /// - /// The postal/ZIP code of the location. - /// - public string PostalCode { get; set; } = string.Empty; + /// + /// The postal/ZIP code of the location. + /// + public string PostalCode { get; set; } = string.Empty; - /// - /// Foreign key referencing the city in which the brewery is located. Maps to CityID. - /// - public Guid CityId { get; set; } + /// + /// Foreign key referencing the city in which the brewery is located. Maps to CityID. + /// + public Guid CityId { get; set; } - /// - /// Serialized SQL Server GEOGRAPHY value representing the geographic coordinates of the location, or - /// null if not set. - /// - public byte[]? Coordinates { get; set; } + /// + /// Serialized SQL Server GEOGRAPHY value representing the geographic coordinates of the location, or + /// null if not set. + /// + public byte[]? Coordinates { get; set; } - /// - /// SQL Server ROWVERSION concurrency token used to detect concurrent updates. null until the row has - /// been read from the database. - /// - public byte[]? Timer { get; set; } -} \ No newline at end of file + /// + /// SQL Server ROWVERSION concurrency token used to detect concurrent updates. null until the row has + /// been read from the database. + /// + public byte[]? Timer { get; set; } +} diff --git a/web/backend/Domain/Domain.Entities/Entities/UserAccount.cs b/web/backend/Domain/Domain.Entities/Entities/UserAccount.cs index acc5090c..6a53e26a 100644 --- a/web/backend/Domain/Domain.Entities/Entities/UserAccount.cs +++ b/web/backend/Domain/Domain.Entities/Entities/UserAccount.cs @@ -7,49 +7,49 @@ namespace Domain.Entities; /// public class UserAccount { - /// - /// Primary key identifying this user account. Maps to UserAccountID. - /// - public Guid UserAccountId { get; set; } + /// + /// Primary key identifying this user account. Maps to UserAccountID. + /// + public Guid UserAccountId { get; set; } - /// - /// The user's unique username, used for login and display. Enforced unique at the database level. - /// - public string Username { get; set; } = string.Empty; + /// + /// The user's unique username, used for login and display. Enforced unique at the database level. + /// + public string Username { get; set; } = string.Empty; - /// - /// The user's first name. - /// - public string FirstName { get; set; } = string.Empty; + /// + /// The user's first name. + /// + public string FirstName { get; set; } = string.Empty; - /// - /// The user's last name. - /// - public string LastName { get; set; } = string.Empty; + /// + /// The user's last name. + /// + public string LastName { get; set; } = string.Empty; - /// - /// The user's email address. Enforced unique at the database level. - /// - public string Email { get; set; } = string.Empty; + /// + /// The user's email address. Enforced unique at the database level. + /// + public string Email { get; set; } = string.Empty; - /// - /// The date and time the account was created. - /// - public DateTime CreatedAt { get; set; } + /// + /// The date and time the account was created. + /// + public DateTime CreatedAt { get; set; } - /// - /// The date and time the account was last updated, or null if it has never been updated. - /// - public DateTime? UpdatedAt { get; set; } + /// + /// The date and time the account was last updated, or null if it has never been updated. + /// + public DateTime? UpdatedAt { get; set; } - /// - /// The user's date of birth. - /// - public DateTime DateOfBirth { get; set; } + /// + /// The user's date of birth. + /// + public DateTime DateOfBirth { get; set; } - /// - /// SQL Server ROWVERSION concurrency token used to detect concurrent updates. null until the row has - /// been read from the database. - /// - public byte[]? Timer { get; set; } -} \ No newline at end of file + /// + /// SQL Server ROWVERSION concurrency token used to detect concurrent updates. null until the row has + /// been read from the database. + /// + public byte[]? Timer { get; set; } +} diff --git a/web/backend/Domain/Domain.Entities/Entities/UserCredential.cs b/web/backend/Domain/Domain.Entities/Entities/UserCredential.cs index 4be575e7..4f1bf69d 100644 --- a/web/backend/Domain/Domain.Entities/Entities/UserCredential.cs +++ b/web/backend/Domain/Domain.Entities/Entities/UserCredential.cs @@ -7,34 +7,34 @@ namespace Domain.Entities; /// public class UserCredential { - /// - /// Primary key identifying this credential. Maps to UserCredentialID. - /// - public Guid UserCredentialId { get; set; } + /// + /// Primary key identifying this credential. Maps to UserCredentialID. + /// + public Guid UserCredentialId { get; set; } - /// - /// Foreign key referencing the owning . Maps to UserAccountID. - /// - public Guid UserAccountId { get; set; } + /// + /// Foreign key referencing the owning . Maps to UserAccountID. + /// + public Guid UserAccountId { get; set; } - /// - /// The date and time the credential was issued. - /// - public DateTime CreatedAt { get; set; } + /// + /// The date and time the credential was issued. + /// + public DateTime CreatedAt { get; set; } - /// - /// The date and time after which the credential is no longer valid. - /// - public DateTime Expiry { get; set; } + /// + /// The date and time after which the credential is no longer valid. + /// + public DateTime Expiry { get; set; } - /// - /// The Argon2 hash of the credential's secret value. The plaintext secret is never persisted. - /// - public string Hash { get; set; } = string.Empty; + /// + /// The Argon2 hash of the credential's secret value. The plaintext secret is never persisted. + /// + public string Hash { get; set; } = string.Empty; - /// - /// SQL Server ROWVERSION concurrency token used to detect concurrent updates. null until the row has - /// been read from the database. - /// - public byte[]? Timer { get; set; } -} \ No newline at end of file + /// + /// SQL Server ROWVERSION concurrency token used to detect concurrent updates. null until the row has + /// been read from the database. + /// + public byte[]? Timer { get; set; } +} diff --git a/web/backend/Domain/Domain.Entities/Entities/UserVerification.cs b/web/backend/Domain/Domain.Entities/Entities/UserVerification.cs index abd5e0ee..b091b074 100644 --- a/web/backend/Domain/Domain.Entities/Entities/UserVerification.cs +++ b/web/backend/Domain/Domain.Entities/Entities/UserVerification.cs @@ -7,25 +7,25 @@ namespace Domain.Entities; /// public class UserVerification { - /// - /// Primary key identifying this verification record. Maps to UserVerificationID. - /// - public Guid UserVerificationId { get; set; } + /// + /// Primary key identifying this verification record. Maps to UserVerificationID. + /// + public Guid UserVerificationId { get; set; } - /// - /// Foreign key referencing the verified . Maps to UserAccountID; unique, enforcing a - /// one-to-one relationship. - /// - public Guid UserAccountId { get; set; } + /// + /// Foreign key referencing the verified . Maps to UserAccountID; unique, enforcing a + /// one-to-one relationship. + /// + public Guid UserAccountId { get; set; } - /// - /// The date and time at which the user account was verified. - /// - public DateTime VerificationDateTime { get; set; } + /// + /// The date and time at which the user account was verified. + /// + public DateTime VerificationDateTime { get; set; } - /// - /// SQL Server ROWVERSION concurrency token used to detect concurrent updates. null until the row has - /// been read from the database. - /// - public byte[]? Timer { get; set; } -} \ No newline at end of file + /// + /// SQL Server ROWVERSION concurrency token used to detect concurrent updates. null until the row has + /// been read from the database. + /// + public byte[]? Timer { get; set; } +} diff --git a/web/backend/Domain/Domain.Exceptions/Domain.Exceptions.csproj b/web/backend/Domain/Domain.Exceptions/Domain.Exceptions.csproj index 237d6616..15dbf6d0 100644 --- a/web/backend/Domain/Domain.Exceptions/Domain.Exceptions.csproj +++ b/web/backend/Domain/Domain.Exceptions/Domain.Exceptions.csproj @@ -1,9 +1,7 @@  - net10.0 enable enable - diff --git a/web/backend/Domain/Domain.Exceptions/Exceptions.cs b/web/backend/Domain/Domain.Exceptions/Exceptions.cs index cf6964b8..a3213530 100644 --- a/web/backend/Domain/Domain.Exceptions/Exceptions.cs +++ b/web/backend/Domain/Domain.Exceptions/Exceptions.cs @@ -30,4 +30,4 @@ public class ForbiddenException(string message) : Exception(message); /// Exception thrown when business rule validation fails (distinct from FluentValidation). /// Maps to HTTP 400 Bad Request. /// -public class ValidationException(string message) : Exception(message); \ No newline at end of file +public class ValidationException(string message) : Exception(message); diff --git a/web/backend/Features/Features.Auth.Tests/Commands/ConfirmUserHandlerTests.cs b/web/backend/Features/Features.Auth.Tests/Commands/ConfirmUserHandlerTests.cs index b6988a43..9ddbe72a 100644 --- a/web/backend/Features/Features.Auth.Tests/Commands/ConfirmUserHandlerTests.cs +++ b/web/backend/Features/Features.Auth.Tests/Commands/ConfirmUserHandlerTests.cs @@ -30,7 +30,7 @@ public class ConfirmUserHandlerTests { new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()), new Claim(JwtRegisteredClaimNames.UniqueName, username), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; ClaimsPrincipal principal = new(new ClaimsIdentity(claims)); return new ValidatedToken(userId, username, principal); @@ -46,10 +46,15 @@ public class ConfirmUserHandlerTests ValidatedToken validatedToken = MakeValidatedToken(userId, username); UserAccount userAccount = new() { UserAccountId = userId, Username = username }; - _tokenServiceMock.Setup(x => x.ValidateConfirmationTokenAsync(confirmationToken)).ReturnsAsync(validatedToken); + _tokenServiceMock + .Setup(x => x.ValidateConfirmationTokenAsync(confirmationToken)) + .ReturnsAsync(validatedToken); _authRepositoryMock.Setup(x => x.ConfirmUserAccountAsync(userId)).ReturnsAsync(userAccount); - ConfirmationPayload result = await _handler.Handle(new ConfirmUserCommand(confirmationToken), CancellationToken.None); + ConfirmationPayload result = await _handler.Handle( + new ConfirmUserCommand(confirmationToken), + CancellationToken.None + ); result.Should().NotBeNull(); result.UserAccountId.Should().Be(userId); @@ -64,7 +69,8 @@ public class ConfirmUserHandlerTests .Setup(x => x.ValidateConfirmationTokenAsync(invalidToken)) .ThrowsAsync(new UnauthorizedException("Invalid confirmation token")); - Func> act = async () => await _handler.Handle(new ConfirmUserCommand(invalidToken), CancellationToken.None); + Func> act = async () => + await _handler.Handle(new ConfirmUserCommand(invalidToken), CancellationToken.None); await act.Should().ThrowAsync(); } @@ -79,10 +85,18 @@ public class ConfirmUserHandlerTests _tokenServiceMock .Setup(x => x.ValidateConfirmationTokenAsync(confirmationToken)) .ReturnsAsync(MakeValidatedToken(userId, username)); - _authRepositoryMock.Setup(x => x.ConfirmUserAccountAsync(userId)).ReturnsAsync((UserAccount?)null); + _authRepositoryMock + .Setup(x => x.ConfirmUserAccountAsync(userId)) + .ReturnsAsync((UserAccount?)null); - Func> act = async () => await _handler.Handle(new ConfirmUserCommand(confirmationToken), CancellationToken.None); + Func> act = async () => + await _handler.Handle( + new ConfirmUserCommand(confirmationToken), + CancellationToken.None + ); - await act.Should().ThrowAsync().WithMessage("*User account not found*"); + await act.Should() + .ThrowAsync() + .WithMessage("*User account not found*"); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth.Tests/Commands/RefreshTokenHandlerTests.cs b/web/backend/Features/Features.Auth.Tests/Commands/RefreshTokenHandlerTests.cs index 610a23f5..8eb5b16c 100644 --- a/web/backend/Features/Features.Auth.Tests/Commands/RefreshTokenHandlerTests.cs +++ b/web/backend/Features/Features.Auth.Tests/Commands/RefreshTokenHandlerTests.cs @@ -21,11 +21,14 @@ public class RefreshTokenHandlerTests .Setup(x => x.RefreshTokenAsync("old-refresh-token")) .ReturnsAsync(new RefreshTokenResult(user, "new-refresh-token", "new-access-token")); - LoginPayload result = await handler.Handle(new RefreshTokenCommand("old-refresh-token"), CancellationToken.None); + LoginPayload result = await handler.Handle( + new RefreshTokenCommand("old-refresh-token"), + CancellationToken.None + ); result.UserAccountId.Should().Be(userId); result.Username.Should().Be("testuser"); result.RefreshToken.Should().Be("new-refresh-token"); result.AccessToken.Should().Be("new-access-token"); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth.Tests/Commands/RegisterUserHandlerTests.cs b/web/backend/Features/Features.Auth.Tests/Commands/RegisterUserHandlerTests.cs index c24bfbc1..999c3c7e 100644 --- a/web/backend/Features/Features.Auth.Tests/Commands/RegisterUserHandlerTests.cs +++ b/web/backend/Features/Features.Auth.Tests/Commands/RegisterUserHandlerTests.cs @@ -35,9 +35,19 @@ public class RegisterUserHandlerTests ); } - private static RegisterUserCommand ValidCommand(string username = "newuser", string email = "john.doe@example.com") + private static RegisterUserCommand ValidCommand( + string username = "newuser", + string email = "john.doe@example.com" + ) { - return new RegisterUserCommand(username, "John", "Doe", email, new DateTime(1990, 1, 1), "SecurePassword123!"); + return new RegisterUserCommand( + username, + "John", + "Doe", + email, + new DateTime(1990, 1, 1), + "SecurePassword123!" + ); } [Fact] @@ -47,27 +57,46 @@ public class RegisterUserHandlerTests const string hashedPassword = "hashed_password_value"; Guid expectedUserId = Guid.NewGuid(); - _authRepoMock.Setup(x => x.GetUserByUsernameAsync(command.Username)).ReturnsAsync((UserAccount?)null); - _authRepoMock.Setup(x => x.GetUserByEmailAsync(command.Email)).ReturnsAsync((UserAccount?)null); + _authRepoMock + .Setup(x => x.GetUserByUsernameAsync(command.Username)) + .ReturnsAsync((UserAccount?)null); + _authRepoMock + .Setup(x => x.GetUserByEmailAsync(command.Email)) + .ReturnsAsync((UserAccount?)null); _passwordInfraMock.Setup(x => x.Hash(command.Password)).Returns(hashedPassword); _authRepoMock - .Setup(x => x.RegisterUserAsync(command.Username, command.FirstName, command.LastName, command.Email, - command.DateOfBirth, hashedPassword)) - .ReturnsAsync(new UserAccount - { - UserAccountId = expectedUserId, - Username = command.Username, - FirstName = command.FirstName, - LastName = command.LastName, - Email = command.Email, - DateOfBirth = command.DateOfBirth, - CreatedAt = DateTime.UtcNow - }); + .Setup(x => + x.RegisterUserAsync( + command.Username, + command.FirstName, + command.LastName, + command.Email, + command.DateOfBirth, + hashedPassword + ) + ) + .ReturnsAsync( + new UserAccount + { + UserAccountId = expectedUserId, + Username = command.Username, + FirstName = command.FirstName, + LastName = command.LastName, + Email = command.Email, + DateOfBirth = command.DateOfBirth, + CreatedAt = DateTime.UtcNow, + } + ); - _tokenServiceMock.Setup(x => x.GenerateAccessToken(It.IsAny())).Returns("access-token"); - _tokenServiceMock.Setup(x => x.GenerateRefreshToken(It.IsAny())).Returns("refresh-token"); - _tokenServiceMock.Setup(x => x.GenerateConfirmationToken(It.IsAny())) + _tokenServiceMock + .Setup(x => x.GenerateAccessToken(It.IsAny())) + .Returns("access-token"); + _tokenServiceMock + .Setup(x => x.GenerateRefreshToken(It.IsAny())) + .Returns("refresh-token"); + _tokenServiceMock + .Setup(x => x.GenerateConfirmationToken(It.IsAny())) .Returns("confirmation-token"); RegistrationPayload result = await _handler.Handle(command, CancellationToken.None); @@ -89,18 +118,36 @@ public class RegisterUserHandlerTests public async Task Handle_WithExistingUsername_ThrowsConflictException() { RegisterUserCommand command = ValidCommand("existinguser"); - UserAccount existingUser = new() { UserAccountId = Guid.NewGuid(), Username = "existinguser" }; + UserAccount existingUser = new() + { + UserAccountId = Guid.NewGuid(), + Username = "existinguser", + }; - _authRepoMock.Setup(x => x.GetUserByUsernameAsync(command.Username)).ReturnsAsync(existingUser); - _authRepoMock.Setup(x => x.GetUserByEmailAsync(command.Email)).ReturnsAsync((UserAccount?)null); + _authRepoMock + .Setup(x => x.GetUserByUsernameAsync(command.Username)) + .ReturnsAsync(existingUser); + _authRepoMock + .Setup(x => x.GetUserByEmailAsync(command.Email)) + .ReturnsAsync((UserAccount?)null); - Func> act = async () => await _handler.Handle(command, CancellationToken.None); + Func> act = async () => + await _handler.Handle(command, CancellationToken.None); - await act.Should().ThrowAsync().WithMessage("Username or email already exists"); + await act.Should() + .ThrowAsync() + .WithMessage("Username or email already exists"); _authRepoMock.Verify( - x => x.RegisterUserAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), + x => + x.RegisterUserAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), Times.Never ); } @@ -109,14 +156,23 @@ public class RegisterUserHandlerTests public async Task Handle_WithExistingEmail_ThrowsConflictException() { RegisterUserCommand command = ValidCommand(email: "existing@example.com"); - UserAccount existingUser = new() { UserAccountId = Guid.NewGuid(), Email = "existing@example.com" }; + UserAccount existingUser = new() + { + UserAccountId = Guid.NewGuid(), + Email = "existing@example.com", + }; - _authRepoMock.Setup(x => x.GetUserByUsernameAsync(command.Username)).ReturnsAsync((UserAccount?)null); + _authRepoMock + .Setup(x => x.GetUserByUsernameAsync(command.Username)) + .ReturnsAsync((UserAccount?)null); _authRepoMock.Setup(x => x.GetUserByEmailAsync(command.Email)).ReturnsAsync(existingUser); - Func> act = async () => await _handler.Handle(command, CancellationToken.None); + Func> act = async () => + await _handler.Handle(command, CancellationToken.None); - await act.Should().ThrowAsync().WithMessage("Username or email already exists"); + await act.Should() + .ThrowAsync() + .WithMessage("Username or email already exists"); } [Fact] @@ -125,24 +181,47 @@ public class RegisterUserHandlerTests RegisterUserCommand command = ValidCommand(); const string hashedPassword = "hashed_secure_password"; - _authRepoMock.Setup(x => x.GetUserByUsernameAsync(It.IsAny())).ReturnsAsync((UserAccount?)null); - _authRepoMock.Setup(x => x.GetUserByEmailAsync(It.IsAny())).ReturnsAsync((UserAccount?)null); + _authRepoMock + .Setup(x => x.GetUserByUsernameAsync(It.IsAny())) + .ReturnsAsync((UserAccount?)null); + _authRepoMock + .Setup(x => x.GetUserByEmailAsync(It.IsAny())) + .ReturnsAsync((UserAccount?)null); _passwordInfraMock.Setup(x => x.Hash(command.Password)).Returns(hashedPassword); _authRepoMock - .Setup(x => x.RegisterUserAsync(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), hashedPassword)) + .Setup(x => + x.RegisterUserAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + hashedPassword + ) + ) .ReturnsAsync(new UserAccount { UserAccountId = Guid.NewGuid() }); - _tokenServiceMock.Setup(x => x.GenerateAccessToken(It.IsAny())).Returns("access-token"); - _tokenServiceMock.Setup(x => x.GenerateRefreshToken(It.IsAny())).Returns("refresh-token"); + _tokenServiceMock + .Setup(x => x.GenerateAccessToken(It.IsAny())) + .Returns("access-token"); + _tokenServiceMock + .Setup(x => x.GenerateRefreshToken(It.IsAny())) + .Returns("refresh-token"); await _handler.Handle(command, CancellationToken.None); _passwordInfraMock.Verify(x => x.Hash(command.Password), Times.Once); _authRepoMock.Verify( - x => x.RegisterUserAsync(command.Username, command.FirstName, command.LastName, command.Email, - command.DateOfBirth, hashedPassword), + x => + x.RegisterUserAsync( + command.Username, + command.FirstName, + command.LastName, + command.Email, + command.DateOfBirth, + hashedPassword + ), Times.Once ); } @@ -152,19 +231,43 @@ public class RegisterUserHandlerTests { RegisterUserCommand command = ValidCommand(); - _authRepoMock.Setup(x => x.GetUserByUsernameAsync(It.IsAny())).ReturnsAsync((UserAccount?)null); - _authRepoMock.Setup(x => x.GetUserByEmailAsync(It.IsAny())).ReturnsAsync((UserAccount?)null); + _authRepoMock + .Setup(x => x.GetUserByUsernameAsync(It.IsAny())) + .ReturnsAsync((UserAccount?)null); + _authRepoMock + .Setup(x => x.GetUserByEmailAsync(It.IsAny())) + .ReturnsAsync((UserAccount?)null); _passwordInfraMock.Setup(x => x.Hash(It.IsAny())).Returns("hashed"); _authRepoMock - .Setup(x => x.RegisterUserAsync(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new UserAccount - { UserAccountId = Guid.NewGuid(), Username = command.Username, Email = command.Email }); + .Setup(x => + x.RegisterUserAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync( + new UserAccount + { + UserAccountId = Guid.NewGuid(), + Username = command.Username, + Email = command.Email, + } + ); - _tokenServiceMock.Setup(x => x.GenerateAccessToken(It.IsAny())).Returns("access-token"); - _tokenServiceMock.Setup(x => x.GenerateRefreshToken(It.IsAny())).Returns("refresh-token"); + _tokenServiceMock + .Setup(x => x.GenerateAccessToken(It.IsAny())) + .Returns("access-token"); + _tokenServiceMock + .Setup(x => x.GenerateRefreshToken(It.IsAny())) + .Returns("refresh-token"); _mediatorMock - .Setup(x => x.Send(It.IsAny(), It.IsAny())) + .Setup(x => + x.Send(It.IsAny(), It.IsAny()) + ) .ThrowsAsync(new Exception("smtp down")); RegistrationPayload result = await _handler.Handle(command, CancellationToken.None); @@ -172,4 +275,4 @@ public class RegisterUserHandlerTests result.ConfirmationEmailSent.Should().BeFalse(); result.AccessToken.Should().Be("access-token"); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth.Tests/Commands/ResendConfirmationEmailHandlerTests.cs b/web/backend/Features/Features.Auth.Tests/Commands/ResendConfirmationEmailHandlerTests.cs index 50289cd7..56147230 100644 --- a/web/backend/Features/Features.Auth.Tests/Commands/ResendConfirmationEmailHandlerTests.cs +++ b/web/backend/Features/Features.Auth.Tests/Commands/ResendConfirmationEmailHandlerTests.cs @@ -17,15 +17,23 @@ public class ResendConfirmationEmailHandlerTests public ResendConfirmationEmailHandlerTests() { - _handler = new ResendConfirmationEmailHandler(_authRepositoryMock.Object, _tokenServiceMock.Object, - _mediatorMock.Object); + _handler = new ResendConfirmationEmailHandler( + _authRepositoryMock.Object, + _tokenServiceMock.Object, + _mediatorMock.Object + ); } [Fact] public async Task Handle_SendsFreshConfirmationEmail_WhenUserExistsAndUnverified() { Guid userId = Guid.NewGuid(); - UserAccount user = new() { UserAccountId = userId, FirstName = "Aaron", Email = "aaron@example.com" }; + UserAccount user = new() + { + UserAccountId = userId, + FirstName = "Aaron", + Email = "aaron@example.com", + }; _authRepositoryMock.Setup(x => x.GetUserByIdAsync(userId)).ReturnsAsync(user); _authRepositoryMock.Setup(x => x.IsUserVerifiedAsync(userId)).ReturnsAsync(false); @@ -34,9 +42,15 @@ public class ResendConfirmationEmailHandlerTests await _handler.Handle(new ResendConfirmationEmailCommand(userId), CancellationToken.None); _mediatorMock.Verify( - x => x.Send(It.Is(c => - c.FirstName == "Aaron" && c.Email == "aaron@example.com" && c.ConfirmationToken == "fresh-token" - ), It.IsAny()), + x => + x.Send( + It.Is(c => + c.FirstName == "Aaron" + && c.Email == "aaron@example.com" + && c.ConfirmationToken == "fresh-token" + ), + It.IsAny() + ), Times.Once ); } @@ -50,7 +64,11 @@ public class ResendConfirmationEmailHandlerTests await _handler.Handle(new ResendConfirmationEmailCommand(userId), CancellationToken.None); _mediatorMock.Verify( - x => x.Send(It.IsAny(), It.IsAny()), + x => + x.Send( + It.IsAny(), + It.IsAny() + ), Times.Never ); } @@ -67,8 +85,12 @@ public class ResendConfirmationEmailHandlerTests await _handler.Handle(new ResendConfirmationEmailCommand(userId), CancellationToken.None); _mediatorMock.Verify( - x => x.Send(It.IsAny(), It.IsAny()), + x => + x.Send( + It.IsAny(), + It.IsAny() + ), Times.Never ); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth.Tests/Features.Auth.Tests.csproj b/web/backend/Features/Features.Auth.Tests/Features.Auth.Tests.csproj index c788b588..7a858bbd 100644 --- a/web/backend/Features/Features.Auth.Tests/Features.Auth.Tests.csproj +++ b/web/backend/Features/Features.Auth.Tests/Features.Auth.Tests.csproj @@ -8,19 +8,19 @@ - - - - - - + + + + + + - + - + diff --git a/web/backend/Features/Features.Auth.Tests/Queries/LoginHandlerTests.cs b/web/backend/Features/Features.Auth.Tests/Queries/LoginHandlerTests.cs index 64d6e013..6fce931c 100644 --- a/web/backend/Features/Features.Auth.Tests/Queries/LoginHandlerTests.cs +++ b/web/backend/Features/Features.Auth.Tests/Queries/LoginHandlerTests.cs @@ -22,7 +22,11 @@ public class LoginHandlerTests _authRepoMock = new Mock(); _passwordInfraMock = new Mock(); _tokenServiceMock = new Mock(); - _handler = new LoginHandler(_authRepoMock.Object, _passwordInfraMock.Object, _tokenServiceMock.Object); + _handler = new LoginHandler( + _authRepoMock.Object, + _passwordInfraMock.Object, + _tokenServiceMock.Object + ); } [Fact] @@ -38,7 +42,7 @@ public class LoginHandlerTests FirstName = "René", LastName = "Descartes", Email = "r.descartes@example.com", - DateOfBirth = new DateTime(1596, 03, 31) + DateOfBirth = new DateTime(1596, 03, 31), }; UserCredential userCredential = new() @@ -46,16 +50,27 @@ public class LoginHandlerTests UserCredentialId = Guid.NewGuid(), UserAccountId = userAccountId, Hash = "some-hash", - Expiry = DateTime.MaxValue + Expiry = DateTime.MaxValue, }; _authRepoMock.Setup(x => x.GetUserByUsernameAsync(username)).ReturnsAsync(userAccount); - _authRepoMock.Setup(x => x.GetActiveCredentialByUserAccountIdAsync(userAccountId)).ReturnsAsync(userCredential); - _passwordInfraMock.Setup(x => x.Verify(It.IsAny(), It.IsAny())).Returns(true); - _tokenServiceMock.Setup(x => x.GenerateAccessToken(It.IsAny())).Returns("access-token"); - _tokenServiceMock.Setup(x => x.GenerateRefreshToken(It.IsAny())).Returns("refresh-token"); + _authRepoMock + .Setup(x => x.GetActiveCredentialByUserAccountIdAsync(userAccountId)) + .ReturnsAsync(userCredential); + _passwordInfraMock + .Setup(x => x.Verify(It.IsAny(), It.IsAny())) + .Returns(true); + _tokenServiceMock + .Setup(x => x.GenerateAccessToken(It.IsAny())) + .Returns("access-token"); + _tokenServiceMock + .Setup(x => x.GenerateRefreshToken(It.IsAny())) + .Returns("refresh-token"); - LoginPayload result = await _handler.Handle(new LoginQuery(username, "any-password"), CancellationToken.None); + LoginPayload result = await _handler.Handle( + new LoginQuery(username, "any-password"), + CancellationToken.None + ); result.Should().NotBeNull(); result.UserAccountId.Should().Be(userAccountId); @@ -68,12 +83,18 @@ public class LoginHandlerTests public async Task Handle_WithUnregisteredUsername_ThrowsUnauthorizedException() { const string username = "de_beauvoir"; - _authRepoMock.Setup(x => x.GetUserByUsernameAsync(username)).ReturnsAsync((UserAccount?)null); + _authRepoMock + .Setup(x => x.GetUserByUsernameAsync(username)) + .ReturnsAsync((UserAccount?)null); - Func> act = async () => await _handler.Handle(new LoginQuery(username, "password"), CancellationToken.None); + Func> act = async () => + await _handler.Handle(new LoginQuery(username, "password"), CancellationToken.None); await act.Should().ThrowAsync(); - _authRepoMock.Verify(x => x.GetActiveCredentialByUserAccountIdAsync(It.IsAny()), Times.Never); + _authRepoMock.Verify( + x => x.GetActiveCredentialByUserAccountIdAsync(It.IsAny()), + Times.Never + ); } [Fact] @@ -84,13 +105,20 @@ public class LoginHandlerTests UserAccount userAccount = new() { UserAccountId = userAccountId, Username = username }; _authRepoMock.Setup(x => x.GetUserByUsernameAsync(username)).ReturnsAsync(userAccount); - _authRepoMock.Setup(x => x.GetActiveCredentialByUserAccountIdAsync(userAccountId)) + _authRepoMock + .Setup(x => x.GetActiveCredentialByUserAccountIdAsync(userAccountId)) .ReturnsAsync((UserCredential?)null); - Func> act = async () => await _handler.Handle(new LoginQuery(username, "password"), CancellationToken.None); + Func> act = async () => + await _handler.Handle(new LoginQuery(username, "password"), CancellationToken.None); - await act.Should().ThrowAsync().WithMessage("Invalid username or password."); - _passwordInfraMock.Verify(x => x.Verify(It.IsAny(), It.IsAny()), Times.Never); + await act.Should() + .ThrowAsync() + .WithMessage("Invalid username or password."); + _passwordInfraMock.Verify( + x => x.Verify(It.IsAny(), It.IsAny()), + Times.Never + ); } [Fact] @@ -99,14 +127,29 @@ public class LoginHandlerTests const string username = "RCarnap"; Guid userAccountId = Guid.NewGuid(); UserAccount userAccount = new() { UserAccountId = userAccountId, Username = username }; - UserCredential userCredential = new() { UserCredentialId = Guid.NewGuid(), UserAccountId = userAccountId, Hash = "hashed-password" }; + UserCredential userCredential = new() + { + UserCredentialId = Guid.NewGuid(), + UserAccountId = userAccountId, + Hash = "hashed-password", + }; _authRepoMock.Setup(x => x.GetUserByUsernameAsync(username)).ReturnsAsync(userAccount); - _authRepoMock.Setup(x => x.GetActiveCredentialByUserAccountIdAsync(userAccountId)).ReturnsAsync(userCredential); - _passwordInfraMock.Setup(x => x.Verify(It.IsAny(), It.IsAny())).Returns(false); + _authRepoMock + .Setup(x => x.GetActiveCredentialByUserAccountIdAsync(userAccountId)) + .ReturnsAsync(userCredential); + _passwordInfraMock + .Setup(x => x.Verify(It.IsAny(), It.IsAny())) + .Returns(false); - Func> act = async () => await _handler.Handle(new LoginQuery(username, "wrong-password"), CancellationToken.None); + Func> act = async () => + await _handler.Handle( + new LoginQuery(username, "wrong-password"), + CancellationToken.None + ); - await act.Should().ThrowAsync().WithMessage("Invalid username or password."); + await act.Should() + .ThrowAsync() + .WithMessage("Invalid username or password."); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth.Tests/Repository/AuthRepositoryTests.cs b/web/backend/Features/Features.Auth.Tests/Repository/AuthRepositoryTests.cs index b1e133c4..1bc7e702 100644 --- a/web/backend/Features/Features.Auth.Tests/Repository/AuthRepositoryTests.cs +++ b/web/backend/Features/Features.Auth.Tests/Repository/AuthRepositoryTests.cs @@ -18,8 +18,7 @@ public class AuthRepositoryTests Guid expectedUserId = Guid.NewGuid(); MockDbConnection conn = new(); - conn.Mocks.When(cmd => cmd.CommandText == "USP_RegisterUser") - .ReturnsScalar(expectedUserId); + conn.Mocks.When(cmd => cmd.CommandText == "USP_RegisterUser").ReturnsScalar(expectedUserId); // Mock the subsequent read for the newly created user by id conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountById") @@ -132,9 +131,7 @@ public class AuthRepositoryTests Guid userId = Guid.NewGuid(); MockDbConnection conn = new(); - conn.Mocks.When(cmd => - cmd.CommandText == "usp_GetUserAccountByUsername" - ) + conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername") .ReturnsTable( MockTable .WithColumns( @@ -175,9 +172,7 @@ public class AuthRepositoryTests { MockDbConnection conn = new(); - conn.Mocks.When(cmd => - cmd.CommandText == "usp_GetUserAccountByUsername" - ) + conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername") .ReturnsTable(MockTable.Empty()); AuthRepository repo = CreateRepo(conn); @@ -193,9 +188,7 @@ public class AuthRepositoryTests Guid credentialId = Guid.NewGuid(); MockDbConnection conn = new(); - conn.Mocks.When(cmd => - cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId" - ) + conn.Mocks.When(cmd => cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId") .ReturnsTable( MockTable .WithColumns( @@ -205,13 +198,7 @@ public class AuthRepositoryTests ("CreatedAt", typeof(DateTime)), ("Timer", typeof(byte[])) ) - .AddRow( - credentialId, - userId, - "hashed_password_value", - DateTime.UtcNow, - null - ) + .AddRow(credentialId, userId, "hashed_password_value", DateTime.UtcNow, null) ); AuthRepository repo = CreateRepo(conn); @@ -229,9 +216,7 @@ public class AuthRepositoryTests Guid userId = Guid.NewGuid(); MockDbConnection conn = new(); - conn.Mocks.When(cmd => - cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId" - ) + conn.Mocks.When(cmd => cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId") .ReturnsTable(MockTable.Empty()); AuthRepository repo = CreateRepo(conn); @@ -247,14 +232,12 @@ public class AuthRepositoryTests string newPasswordHash = "new_hashed_password"; MockDbConnection conn = new(); - conn.Mocks.When(cmd => cmd.CommandText == "USP_RotateUserCredential") - .ReturnsScalar(1); + conn.Mocks.When(cmd => cmd.CommandText == "USP_RotateUserCredential").ReturnsScalar(1); AuthRepository repo = CreateRepo(conn); // Should not throw - Func act = async () => - await repo.RotateCredentialAsync(userId, newPasswordHash); + Func act = async () => await repo.RotateCredentialAsync(userId, newPasswordHash); await act.Should().NotThrowAsync(); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth.Tests/Repository/TestConnectionFactory.cs b/web/backend/Features/Features.Auth.Tests/Repository/TestConnectionFactory.cs index a20e0824..95e816df 100644 --- a/web/backend/Features/Features.Auth.Tests/Repository/TestConnectionFactory.cs +++ b/web/backend/Features/Features.Auth.Tests/Repository/TestConnectionFactory.cs @@ -11,4 +11,4 @@ internal class TestConnectionFactory(DbConnection conn) : ISqlConnectionFactory { return _conn; } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth.Tests/Services/TokenServiceRefreshTests.cs b/web/backend/Features/Features.Auth.Tests/Services/TokenServiceRefreshTests.cs index ac29471c..76c49ab9 100644 --- a/web/backend/Features/Features.Auth.Tests/Services/TokenServiceRefreshTests.cs +++ b/web/backend/Features/Features.Auth.Tests/Services/TokenServiceRefreshTests.cs @@ -22,15 +22,20 @@ public class TokenServiceRefreshTests _authRepositoryMock = new Mock(); // Set environment variables for tokens - Environment.SetEnvironmentVariable("ACCESS_TOKEN_SECRET", "test-access-secret-that-is-very-long-1234567890"); - Environment.SetEnvironmentVariable("REFRESH_TOKEN_SECRET", "test-refresh-secret-that-is-very-long-1234567890"); - Environment.SetEnvironmentVariable("CONFIRMATION_TOKEN_SECRET", - "test-confirmation-secret-that-is-very-long-1234567890"); - - _tokenService = new TokenService( - _tokenInfraMock.Object, - _authRepositoryMock.Object + Environment.SetEnvironmentVariable( + "ACCESS_TOKEN_SECRET", + "test-access-secret-that-is-very-long-1234567890" ); + Environment.SetEnvironmentVariable( + "REFRESH_TOKEN_SECRET", + "test-refresh-secret-that-is-very-long-1234567890" + ); + Environment.SetEnvironmentVariable( + "CONFIRMATION_TOKEN_SECRET", + "test-confirmation-secret-that-is-very-long-1234567890" + ); + + _tokenService = new TokenService(_tokenInfraMock.Object, _authRepositoryMock.Object); } [Fact] @@ -45,7 +50,7 @@ public class TokenServiceRefreshTests { new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()), new Claim(JwtRegisteredClaimNames.UniqueName, username), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; ClaimsIdentity claimsIdentity = new(claims); @@ -58,7 +63,7 @@ public class TokenServiceRefreshTests FirstName = "Test", LastName = "User", Email = "test@example.com", - DateOfBirth = new DateTime(1990, 1, 1) + DateOfBirth = new DateTime(1990, 1, 1), }; // Mock the validation of refresh token @@ -69,11 +74,11 @@ public class TokenServiceRefreshTests // Mock the generation of new tokens _tokenInfraMock .Setup(x => x.GenerateJwt(userId, username, It.IsAny(), It.IsAny())) - .Returns((Guid _, string _, DateTime _, string _) => $"generated-token-{Guid.NewGuid()}"); + .Returns( + (Guid _, string _, DateTime _, string _) => $"generated-token-{Guid.NewGuid()}" + ); - _authRepositoryMock - .Setup(x => x.GetUserByIdAsync(userId)) - .ReturnsAsync(userAccount); + _authRepositoryMock.Setup(x => x.GetUserByIdAsync(userId)).ReturnsAsync(userAccount); // Act RefreshTokenResult result = await _tokenService.RefreshTokenAsync(refreshToken); @@ -85,14 +90,17 @@ public class TokenServiceRefreshTests result.AccessToken.Should().NotBeEmpty(); result.RefreshToken.Should().NotBeEmpty(); - _authRepositoryMock.Verify( - x => x.GetUserByIdAsync(userId), - Times.Once - ); + _authRepositoryMock.Verify(x => x.GetUserByIdAsync(userId), Times.Once); // Verify tokens were generated (called twice - once for access, once for refresh) _tokenInfraMock.Verify( - x => x.GenerateJwt(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + x => + x.GenerateJwt( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), Times.Exactly(2) ); } @@ -108,9 +116,10 @@ public class TokenServiceRefreshTests .ThrowsAsync(new UnauthorizedException("Invalid refresh token")); // Act & Assert - await FluentActions.Invoking(async () => - await _tokenService.RefreshTokenAsync(invalidToken) - ).Should().ThrowAsync(); + await FluentActions + .Invoking(async () => await _tokenService.RefreshTokenAsync(invalidToken)) + .Should() + .ThrowAsync(); } [Fact] @@ -124,9 +133,10 @@ public class TokenServiceRefreshTests .ThrowsAsync(new UnauthorizedException("Refresh token has expired")); // Act & Assert - await FluentActions.Invoking(async () => - await _tokenService.RefreshTokenAsync(expiredToken) - ).Should().ThrowAsync(); + await FluentActions + .Invoking(async () => await _tokenService.RefreshTokenAsync(expiredToken)) + .Should() + .ThrowAsync(); } [Fact] @@ -141,7 +151,7 @@ public class TokenServiceRefreshTests { new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()), new Claim(JwtRegisteredClaimNames.UniqueName, username), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; ClaimsIdentity claimsIdentity = new(claims); @@ -151,14 +161,13 @@ public class TokenServiceRefreshTests .Setup(x => x.ValidateJwtAsync(refreshToken, It.IsAny())) .ReturnsAsync(principal); - _authRepositoryMock - .Setup(x => x.GetUserByIdAsync(userId)) - .ReturnsAsync((UserAccount?)null); + _authRepositoryMock.Setup(x => x.GetUserByIdAsync(userId)).ReturnsAsync((UserAccount?)null); // Act & Assert - await FluentActions.Invoking(async () => - await _tokenService.RefreshTokenAsync(refreshToken) - ).Should().ThrowAsync() + await FluentActions + .Invoking(async () => await _tokenService.RefreshTokenAsync(refreshToken)) + .Should() + .ThrowAsync() .WithMessage("*User account not found*"); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth.Tests/Services/TokenServiceValidationTests.cs b/web/backend/Features/Features.Auth.Tests/Services/TokenServiceValidationTests.cs index f3550cf0..136a2fc5 100644 --- a/web/backend/Features/Features.Auth.Tests/Services/TokenServiceValidationTests.cs +++ b/web/backend/Features/Features.Auth.Tests/Services/TokenServiceValidationTests.cs @@ -21,15 +21,20 @@ public class TokenServiceValidationTests _authRepositoryMock = new Mock(); // Set environment variables for tokens - Environment.SetEnvironmentVariable("ACCESS_TOKEN_SECRET", "test-access-secret-that-is-very-long-1234567890"); - Environment.SetEnvironmentVariable("REFRESH_TOKEN_SECRET", "test-refresh-secret-that-is-very-long-1234567890"); - Environment.SetEnvironmentVariable("CONFIRMATION_TOKEN_SECRET", - "test-confirmation-secret-that-is-very-long-1234567890"); - - _tokenService = new TokenService( - _tokenInfraMock.Object, - _authRepositoryMock.Object + Environment.SetEnvironmentVariable( + "ACCESS_TOKEN_SECRET", + "test-access-secret-that-is-very-long-1234567890" ); + Environment.SetEnvironmentVariable( + "REFRESH_TOKEN_SECRET", + "test-refresh-secret-that-is-very-long-1234567890" + ); + Environment.SetEnvironmentVariable( + "CONFIRMATION_TOKEN_SECRET", + "test-confirmation-secret-that-is-very-long-1234567890" + ); + + _tokenService = new TokenService(_tokenInfraMock.Object, _authRepositoryMock.Object); } [Fact] @@ -44,7 +49,7 @@ public class TokenServiceValidationTests { new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()), new Claim(JwtRegisteredClaimNames.UniqueName, username), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; ClaimsIdentity claimsIdentity = new(claims); @@ -55,15 +60,17 @@ public class TokenServiceValidationTests .ReturnsAsync(principal); // Act - ValidatedToken result = - await _tokenService.ValidateAccessTokenAsync(token); + ValidatedToken result = await _tokenService.ValidateAccessTokenAsync(token); // Assert result.Should().NotBeNull(); result.UserId.Should().Be(userId); result.Username.Should().Be(username); result.Principal.Should().NotBeNull(); - result.Principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value.Should().Be(userId.ToString()); + result + .Principal.FindFirst(JwtRegisteredClaimNames.Sub) + ?.Value.Should() + .Be(userId.ToString()); } [Fact] @@ -78,7 +85,7 @@ public class TokenServiceValidationTests { new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()), new Claim(JwtRegisteredClaimNames.UniqueName, username), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; ClaimsIdentity claimsIdentity = new(claims); @@ -89,8 +96,7 @@ public class TokenServiceValidationTests .ReturnsAsync(principal); // Act - ValidatedToken result = - await _tokenService.ValidateRefreshTokenAsync(token); + ValidatedToken result = await _tokenService.ValidateRefreshTokenAsync(token); // Assert result.Should().NotBeNull(); @@ -110,7 +116,7 @@ public class TokenServiceValidationTests { new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()), new Claim(JwtRegisteredClaimNames.UniqueName, username), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; ClaimsIdentity claimsIdentity = new(claims); @@ -121,8 +127,7 @@ public class TokenServiceValidationTests .ReturnsAsync(principal); // Act - ValidatedToken result = - await _tokenService.ValidateConfirmationTokenAsync(token); + ValidatedToken result = await _tokenService.ValidateConfirmationTokenAsync(token); // Assert result.Should().NotBeNull(); @@ -141,9 +146,10 @@ public class TokenServiceValidationTests .ThrowsAsync(new UnauthorizedException("Invalid token")); // Act & Assert - await FluentActions.Invoking(async () => - await _tokenService.ValidateAccessTokenAsync(token) - ).Should().ThrowAsync(); + await FluentActions + .Invoking(async () => await _tokenService.ValidateAccessTokenAsync(token)) + .Should() + .ThrowAsync(); } [Fact] @@ -154,14 +160,13 @@ public class TokenServiceValidationTests _tokenInfraMock .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) - .ThrowsAsync(new UnauthorizedException( - "Token has expired" - )); + .ThrowsAsync(new UnauthorizedException("Token has expired")); // Act & Assert - await FluentActions.Invoking(async () => - await _tokenService.ValidateAccessTokenAsync(token) - ).Should().ThrowAsync(); + await FluentActions + .Invoking(async () => await _tokenService.ValidateAccessTokenAsync(token)) + .Should() + .ThrowAsync(); } [Fact] @@ -175,7 +180,7 @@ public class TokenServiceValidationTests List claims = new() { new Claim(JwtRegisteredClaimNames.UniqueName, username), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; ClaimsIdentity claimsIdentity = new(claims); @@ -186,9 +191,10 @@ public class TokenServiceValidationTests .ReturnsAsync(principal); // Act & Assert - await FluentActions.Invoking(async () => - await _tokenService.ValidateAccessTokenAsync(token) - ).Should().ThrowAsync() + await FluentActions + .Invoking(async () => await _tokenService.ValidateAccessTokenAsync(token)) + .Should() + .ThrowAsync() .WithMessage("*missing required claims*"); } @@ -203,7 +209,7 @@ public class TokenServiceValidationTests List claims = new() { new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; ClaimsIdentity claimsIdentity = new(claims); @@ -214,9 +220,10 @@ public class TokenServiceValidationTests .ReturnsAsync(principal); // Act & Assert - await FluentActions.Invoking(async () => - await _tokenService.ValidateAccessTokenAsync(token) - ).Should().ThrowAsync() + await FluentActions + .Invoking(async () => await _tokenService.ValidateAccessTokenAsync(token)) + .Should() + .ThrowAsync() .WithMessage("*missing required claims*"); } @@ -232,7 +239,7 @@ public class TokenServiceValidationTests { new Claim(JwtRegisteredClaimNames.Sub, "not-a-valid-guid"), new Claim(JwtRegisteredClaimNames.UniqueName, username), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; ClaimsIdentity claimsIdentity = new(claims); @@ -243,9 +250,10 @@ public class TokenServiceValidationTests .ReturnsAsync(principal); // Act & Assert - await FluentActions.Invoking(async () => - await _tokenService.ValidateAccessTokenAsync(token) - ).Should().ThrowAsync() + await FluentActions + .Invoking(async () => await _tokenService.ValidateAccessTokenAsync(token)) + .Should() + .ThrowAsync() .WithMessage("*malformed user ID*"); } @@ -260,9 +268,10 @@ public class TokenServiceValidationTests .ThrowsAsync(new UnauthorizedException("Invalid token")); // Act & Assert - await FluentActions.Invoking(async () => - await _tokenService.ValidateRefreshTokenAsync(token) - ).Should().ThrowAsync(); + await FluentActions + .Invoking(async () => await _tokenService.ValidateRefreshTokenAsync(token)) + .Should() + .ThrowAsync(); } [Fact] @@ -276,8 +285,9 @@ public class TokenServiceValidationTests .ThrowsAsync(new UnauthorizedException("Invalid token")); // Act & Assert - await FluentActions.Invoking(async () => - await _tokenService.ValidateConfirmationTokenAsync(token) - ).Should().ThrowAsync(); + await FluentActions + .Invoking(async () => await _tokenService.ValidateConfirmationTokenAsync(token)) + .Should() + .ThrowAsync(); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Commands/ConfirmUser/ConfirmUserCommand.cs b/web/backend/Features/Features.Auth/Commands/ConfirmUser/ConfirmUserCommand.cs index bb808680..0a3c3aae 100644 --- a/web/backend/Features/Features.Auth/Commands/ConfirmUser/ConfirmUserCommand.cs +++ b/web/backend/Features/Features.Auth/Commands/ConfirmUser/ConfirmUserCommand.cs @@ -7,4 +7,4 @@ namespace Features.Auth.Commands.ConfirmUser; /// Validates a confirmation token and confirms the corresponding user account. /// /// The confirmation token issued to the user, typically delivered via email. -public record ConfirmUserCommand(string Token) : IRequest; \ No newline at end of file +public record ConfirmUserCommand(string Token) : IRequest; diff --git a/web/backend/Features/Features.Auth/Commands/ConfirmUser/ConfirmUserHandler.cs b/web/backend/Features/Features.Auth/Commands/ConfirmUser/ConfirmUserHandler.cs index 979698dc..230d799a 100644 --- a/web/backend/Features/Features.Auth/Commands/ConfirmUser/ConfirmUserHandler.cs +++ b/web/backend/Features/Features.Auth/Commands/ConfirmUser/ConfirmUserHandler.cs @@ -13,22 +13,26 @@ namespace Features.Auth.Commands.ConfirmUser; /// /// Repository used to look up and confirm user accounts. /// Service used to validate the confirmation token. -public class ConfirmUserHandler( - IAuthRepository authRepository, - ITokenService tokenService -) : IRequestHandler +public class ConfirmUserHandler(IAuthRepository authRepository, ITokenService tokenService) + : IRequestHandler { /// /// Thrown when the confirmation token is invalid or expired, or when the associated user account cannot be found. /// - public async Task Handle(ConfirmUserCommand request, CancellationToken cancellationToken) + public async Task Handle( + ConfirmUserCommand request, + CancellationToken cancellationToken + ) { - ValidatedToken validatedToken = await tokenService.ValidateConfirmationTokenAsync(request.Token); + ValidatedToken validatedToken = await tokenService.ValidateConfirmationTokenAsync( + request.Token + ); UserAccount? user = await authRepository.ConfirmUserAccountAsync(validatedToken.UserId); - if (user == null) throw new UnauthorizedException("User account not found"); + if (user == null) + throw new UnauthorizedException("User account not found"); return new ConfirmationPayload(user.UserAccountId, DateTime.UtcNow); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenCommand.cs b/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenCommand.cs index ed8709e8..d0652354 100644 --- a/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenCommand.cs +++ b/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenCommand.cs @@ -7,4 +7,4 @@ namespace Features.Auth.Commands.RefreshToken; /// Exchanges a valid refresh token for a new access/refresh token pair. Bound directly from the /// request body of POST /api/auth/refresh. /// -public record RefreshTokenCommand(string RefreshToken) : IRequest; \ No newline at end of file +public record RefreshTokenCommand(string RefreshToken) : IRequest; diff --git a/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenHandler.cs b/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenHandler.cs index 977e7393..91c9d496 100644 --- a/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenHandler.cs +++ b/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenHandler.cs @@ -12,10 +12,17 @@ namespace Features.Auth.Commands.RefreshToken; public class RefreshTokenHandler(ITokenService tokenService) : IRequestHandler { - public async Task Handle(RefreshTokenCommand request, CancellationToken cancellationToken) + public async Task Handle( + RefreshTokenCommand request, + CancellationToken cancellationToken + ) { RefreshTokenResult result = await tokenService.RefreshTokenAsync(request.RefreshToken); - return new LoginPayload(result.UserAccount.UserAccountId, result.UserAccount.Username, result.RefreshToken, - result.AccessToken); + return new LoginPayload( + result.UserAccount.UserAccountId, + result.UserAccount.Username, + result.RefreshToken, + result.AccessToken + ); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenValidator.cs b/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenValidator.cs index 24070f5c..b6f2a2e7 100644 --- a/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenValidator.cs +++ b/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenValidator.cs @@ -12,8 +12,6 @@ public class RefreshTokenValidator : AbstractValidator /// public RefreshTokenValidator() { - RuleFor(x => x.RefreshToken) - .NotEmpty() - .WithMessage("Refresh token is required"); + RuleFor(x => x.RefreshToken).NotEmpty().WithMessage("Refresh token is required"); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserCommand.cs b/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserCommand.cs index ab2965a9..c6d7d05c 100644 --- a/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserCommand.cs +++ b/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserCommand.cs @@ -25,4 +25,4 @@ public record RegisterUserCommand( string Email, DateTime DateOfBirth, string Password -) : IRequest; \ No newline at end of file +) : IRequest; diff --git a/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserHandler.cs b/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserHandler.cs index 39915bdc..0adb2990 100644 --- a/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserHandler.cs +++ b/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserHandler.cs @@ -28,7 +28,10 @@ public class RegisterUserHandler( /// /// Thrown when an existing account already has the same username or email address. /// - public async Task Handle(RegisterUserCommand request, CancellationToken cancellationToken) + public async Task Handle( + RegisterUserCommand request, + CancellationToken cancellationToken + ) { await ValidateUserDoesNotExist(request.Username, request.Email); @@ -48,14 +51,23 @@ public class RegisterUserHandler( string confirmationToken = tokenService.GenerateConfirmationToken(createdUser); if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken)) - return new RegistrationPayload(createdUser.UserAccountId, createdUser.Username, string.Empty, string.Empty, - false); + return new RegistrationPayload( + createdUser.UserAccountId, + createdUser.Username, + string.Empty, + string.Empty, + false + ); bool emailSent = false; try { await mediator.Send( - new SendRegistrationEmailCommand(createdUser.FirstName, createdUser.Email, confirmationToken), + new SendRegistrationEmailCommand( + createdUser.FirstName, + createdUser.Email, + confirmationToken + ), cancellationToken ); emailSent = true; @@ -67,8 +79,13 @@ public class RegisterUserHandler( // ignored } - return new RegistrationPayload(createdUser.UserAccountId, createdUser.Username, refreshToken, accessToken, - emailSent); + return new RegistrationPayload( + createdUser.UserAccountId, + createdUser.Username, + refreshToken, + accessToken, + emailSent + ); } /// @@ -82,4 +99,4 @@ public class RegisterUserHandler( if (existingUsername != null || existingEmail != null) throw new ConflictException("Username or email already exists"); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserValidator.cs b/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserValidator.cs index bca2aaa4..04125df8 100644 --- a/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserValidator.cs +++ b/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserValidator.cs @@ -61,8 +61,6 @@ public class RegisterUserValidator : AbstractValidator .Matches("[0-9]") .WithMessage("Password must contain at least one number") .Matches("[^a-zA-Z0-9]") - .WithMessage( - "Password must contain at least one special character" - ); + .WithMessage("Password must contain at least one special character"); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Commands/ResendConfirmationEmail/ResendConfirmationEmailCommand.cs b/web/backend/Features/Features.Auth/Commands/ResendConfirmationEmail/ResendConfirmationEmailCommand.cs index 66c4fbfe..5b276fc0 100644 --- a/web/backend/Features/Features.Auth/Commands/ResendConfirmationEmail/ResendConfirmationEmailCommand.cs +++ b/web/backend/Features/Features.Auth/Commands/ResendConfirmationEmail/ResendConfirmationEmailCommand.cs @@ -6,4 +6,4 @@ namespace Features.Auth.Commands.ResendConfirmationEmail; /// Resends the account confirmation email to a user, generating a fresh confirmation token. /// /// The unique identifier of the user requesting the resend. -public record ResendConfirmationEmailCommand(Guid UserId) : IRequest; \ No newline at end of file +public record ResendConfirmationEmailCommand(Guid UserId) : IRequest; diff --git a/web/backend/Features/Features.Auth/Commands/ResendConfirmationEmail/ResendConfirmationEmailHandler.cs b/web/backend/Features/Features.Auth/Commands/ResendConfirmationEmail/ResendConfirmationEmailHandler.cs index 50492607..33eb3f98 100644 --- a/web/backend/Features/Features.Auth/Commands/ResendConfirmationEmail/ResendConfirmationEmailHandler.cs +++ b/web/backend/Features/Features.Auth/Commands/ResendConfirmationEmail/ResendConfirmationEmailHandler.cs @@ -23,12 +23,17 @@ public class ResendConfirmationEmailHandler( IMediator mediator ) : IRequestHandler { - public async Task Handle(ResendConfirmationEmailCommand request, CancellationToken cancellationToken) + public async Task Handle( + ResendConfirmationEmailCommand request, + CancellationToken cancellationToken + ) { UserAccount? user = await authRepository.GetUserByIdAsync(request.UserId); - if (user == null) return; // Silent return to prevent user enumeration + if (user == null) + return; // Silent return to prevent user enumeration - if (await authRepository.IsUserVerifiedAsync(request.UserId)) return; // Already confirmed, no-op + if (await authRepository.IsUserVerifiedAsync(request.UserId)) + return; // Already confirmed, no-op string confirmationToken = tokenService.GenerateConfirmationToken(user); await mediator.Send( @@ -36,4 +41,4 @@ public class ResendConfirmationEmailHandler( cancellationToken ); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Controllers/AuthController.cs b/web/backend/Features/Features.Auth/Controllers/AuthController.cs index 12fe44a6..308c36d8 100644 --- a/web/backend/Features/Features.Auth/Controllers/AuthController.cs +++ b/web/backend/Features/Features.Auth/Controllers/AuthController.cs @@ -40,11 +40,14 @@ public class AuthController(IMediator mediator) : ControllerBase ) { RegistrationPayload payload = await mediator.Send(command); - return Created("/", new ResponseBody - { - Message = "User registered successfully.", - Payload = payload - }); + return Created( + "/", + new ResponseBody + { + Message = "User registered successfully.", + Payload = payload, + } + ); } /// @@ -61,11 +64,13 @@ public class AuthController(IMediator mediator) : ControllerBase public async Task>> Login([FromBody] LoginQuery query) { LoginPayload payload = await mediator.Send(query); - return Ok(new ResponseBody - { - Message = "Logged in successfully.", - Payload = payload - }); + return Ok( + new ResponseBody + { + Message = "Logged in successfully.", + Payload = payload, + } + ); } /// @@ -78,14 +83,18 @@ public class AuthController(IMediator mediator) : ControllerBase /// The confirmation token supplied via the confirmation email link. /// A 200 OK result wrapping a of . [HttpPost("confirm")] - public async Task>> Confirm([FromQuery] string token) + public async Task>> Confirm( + [FromQuery] string token + ) { ConfirmationPayload payload = await mediator.Send(new ConfirmUserCommand(token)); - return Ok(new ResponseBody - { - Message = "User with ID " + payload.UserAccountId + " is confirmed.", - Payload = payload - }); + return Ok( + new ResponseBody + { + Message = "User with ID " + payload.UserAccountId + " is confirmed.", + Payload = payload, + } + ); } /// @@ -119,10 +128,12 @@ public class AuthController(IMediator mediator) : ControllerBase ) { LoginPayload payload = await mediator.Send(command); - return Ok(new ResponseBody - { - Message = "Token refreshed successfully.", - Payload = payload - }); + return Ok( + new ResponseBody + { + Message = "Token refreshed successfully.", + Payload = payload, + } + ); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/DependencyInjection/FeaturesAuthServiceCollectionExtensions.cs b/web/backend/Features/Features.Auth/DependencyInjection/FeaturesAuthServiceCollectionExtensions.cs index 16c844e4..7e475731 100644 --- a/web/backend/Features/Features.Auth/DependencyInjection/FeaturesAuthServiceCollectionExtensions.cs +++ b/web/backend/Features/Features.Auth/DependencyInjection/FeaturesAuthServiceCollectionExtensions.cs @@ -17,4 +17,4 @@ public static class FeaturesAuthServiceCollectionExtensions services.AddScoped(); return services; } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Dtos/AuthPayloads.cs b/web/backend/Features/Features.Auth/Dtos/AuthPayloads.cs index 0b1bab46..64e5a855 100644 --- a/web/backend/Features/Features.Auth/Dtos/AuthPayloads.cs +++ b/web/backend/Features/Features.Auth/Dtos/AuthPayloads.cs @@ -35,4 +35,4 @@ public record RegistrationPayload( /// /// The unique identifier of the user account that was confirmed. /// The date and time at which the account was confirmed. -public record ConfirmationPayload(Guid UserAccountId, DateTime ConfirmedDate); \ No newline at end of file +public record ConfirmationPayload(Guid UserAccountId, DateTime ConfirmedDate); diff --git a/web/backend/Features/Features.Auth/Features.Auth.csproj b/web/backend/Features/Features.Auth/Features.Auth.csproj index 7adcdc9b..3710b91e 100644 --- a/web/backend/Features/Features.Auth/Features.Auth.csproj +++ b/web/backend/Features/Features.Auth/Features.Auth.csproj @@ -7,16 +7,16 @@ - + - - - - - - - + + + + + + + diff --git a/web/backend/Features/Features.Auth/Queries/Login/LoginHandler.cs b/web/backend/Features/Features.Auth/Queries/Login/LoginHandler.cs index 6306d975..d3ee64d4 100644 --- a/web/backend/Features/Features.Auth/Queries/Login/LoginHandler.cs +++ b/web/backend/Features/Features.Auth/Queries/Login/LoginHandler.cs @@ -46,4 +46,4 @@ public class LoginHandler( return new LoginPayload(user.UserAccountId, user.Username, refreshToken, accessToken); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Queries/Login/LoginQuery.cs b/web/backend/Features/Features.Auth/Queries/Login/LoginQuery.cs index 3ae72cb2..081e6aa9 100644 --- a/web/backend/Features/Features.Auth/Queries/Login/LoginQuery.cs +++ b/web/backend/Features/Features.Auth/Queries/Login/LoginQuery.cs @@ -7,4 +7,4 @@ namespace Features.Auth.Queries.Login; /// Authenticates a user using their username and password and issues new tokens. Bound directly /// from the request body of POST /api/auth/login. /// -public record LoginQuery(string Username, string Password) : IRequest; \ No newline at end of file +public record LoginQuery(string Username, string Password) : IRequest; diff --git a/web/backend/Features/Features.Auth/Queries/Login/LoginValidator.cs b/web/backend/Features/Features.Auth/Queries/Login/LoginValidator.cs index 6b60c07b..8458414e 100644 --- a/web/backend/Features/Features.Auth/Queries/Login/LoginValidator.cs +++ b/web/backend/Features/Features.Auth/Queries/Login/LoginValidator.cs @@ -17,4 +17,4 @@ public class LoginValidator : AbstractValidator RuleFor(x => x.Password).NotEmpty().WithMessage("Password is required"); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Repository/AuthRepository.cs b/web/backend/Features/Features.Auth/Repository/AuthRepository.cs index 7f4eb6a6..03fe1380 100644 --- a/web/backend/Features/Features.Auth/Repository/AuthRepository.cs +++ b/web/backend/Features/Features.Auth/Repository/AuthRepository.cs @@ -81,8 +81,8 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) } } - return await GetUserByIdAsync(userAccountId) ?? - throw new Exception("Failed to retrieve newly registered user."); + return await GetUserByIdAsync(userAccountId) + ?? throw new Exception("Failed to retrieve newly registered user."); } /// @@ -92,9 +92,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) /// Email address to search for /// UserAccount if found, null otherwise /// Thrown when the database command fails. - public async Task GetUserByEmailAsync( - string email - ) + public async Task GetUserByEmailAsync(string email) { await using DbConnection connection = await CreateConnection(); await using DbCommand command = connection.CreateCommand(); @@ -114,9 +112,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) /// Username to search for /// UserAccount if found, null otherwise /// Thrown when the database command fails. - public async Task GetUserByUsernameAsync( - string username - ) + public async Task GetUserByUsernameAsync(string username) { await using DbConnection connection = await CreateConnection(); await using DbCommand command = connection.CreateCommand(); @@ -136,9 +132,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) /// ID of the user account /// Active UserCredential if found, null otherwise /// Thrown when the database command fails. - public async Task GetActiveCredentialByUserAccountIdAsync( - Guid userAccountId - ) + public async Task GetActiveCredentialByUserAccountIdAsync(Guid userAccountId) { await using DbConnection connection = await CreateConnection(); await using DbCommand command = connection.CreateCommand(); @@ -158,10 +152,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) /// ID of the user account /// New hashed password /// Thrown when the database command fails. - public async Task RotateCredentialAsync( - Guid userAccountId, - string newPasswordHash - ) + public async Task RotateCredentialAsync(Guid userAccountId, string newPasswordHash) { await using DbConnection connection = await CreateConnection(); await using DbCommand command = connection.CreateCommand(); @@ -180,9 +171,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) /// ID of the user account /// UserAccount if found, null otherwise /// Thrown when the database command fails. - public async Task GetUserByIdAsync( - Guid userAccountId - ) + public async Task GetUserByIdAsync(Guid userAccountId) { await using DbConnection connection = await CreateConnection(); await using DbCommand command = connection.CreateCommand(); @@ -207,15 +196,15 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) /// Thrown when the database command fails for a reason other than /// a duplicate verification record. /// - public async Task ConfirmUserAccountAsync( - Guid userAccountId - ) + public async Task ConfirmUserAccountAsync(Guid userAccountId) { UserAccount? user = await GetUserByIdAsync(userAccountId); - if (user == null) return null; + if (user == null) + return null; // Idempotency: if already verified, treat as successful confirmation. - if (await IsUserVerifiedAsync(userAccountId)) return user; + if (await IsUserVerifiedAsync(userAccountId)) + return user; await using DbConnection connection = await CreateConnection(); await using DbCommand command = connection.CreateCommand(); @@ -270,15 +259,12 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) return ex.Number == 2601 || ex.Number == 2627; } - /// /// Maps a data reader row to a UserAccount entity. /// /// The data reader positioned on the row to map. /// The mapped instance. - protected override UserAccount MapToEntity( - DbDataReader reader - ) + protected override UserAccount MapToEntity(DbDataReader reader) { return new UserAccount { @@ -292,9 +278,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) ? null : reader.GetDateTime(reader.GetOrdinal("UpdatedAt")), DateOfBirth = reader.GetDateTime(reader.GetOrdinal("DateOfBirth")), - Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) - ? null - : (byte[])reader["Timer"] + Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) ? null : (byte[])reader["Timer"], }; } @@ -308,12 +292,10 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) { UserCredential entity = new() { - UserCredentialId = reader.GetGuid( - reader.GetOrdinal("UserCredentialId") - ), + UserCredentialId = reader.GetGuid(reader.GetOrdinal("UserCredentialId")), UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")), Hash = reader.GetString(reader.GetOrdinal("Hash")), - CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")) + CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")), }; // Optional columns @@ -327,7 +309,8 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) "Timer", StringComparison.OrdinalIgnoreCase ) - ) ?? false; + ) + ?? false; if (hasTimer) entity.Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) @@ -344,15 +327,11 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) /// The command to add the parameter to. /// The parameter name (including any prefix, e.g. "@Username"). /// The parameter value, or null to bind . - private static void AddParameter( - DbCommand command, - string name, - object? value - ) + private static void AddParameter(DbCommand command, string name, object? value) { DbParameter p = command.CreateParameter(); p.ParameterName = name; p.Value = value ?? DBNull.Value; command.Parameters.Add(p); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Repository/IAuthRepository.cs b/web/backend/Features/Features.Auth/Repository/IAuthRepository.cs index 5c791b70..d405891b 100644 --- a/web/backend/Features/Features.Auth/Repository/IAuthRepository.cs +++ b/web/backend/Features/Features.Auth/Repository/IAuthRepository.cs @@ -49,9 +49,7 @@ public interface IAuthRepository /// /// ID of the user account /// Active UserCredential if found, null otherwise - Task GetActiveCredentialByUserAccountIdAsync( - Guid userAccountId - ); + Task GetActiveCredentialByUserAccountIdAsync(Guid userAccountId); /// /// Rotates a user's credential by invalidating all existing credentials and creating a new one. @@ -81,4 +79,4 @@ public interface IAuthRepository /// ID of the user account /// True if the user has a verification record, false otherwise Task IsUserVerifiedAsync(Guid userAccountId); -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Services/ITokenService.cs b/web/backend/Features/Features.Auth/Services/ITokenService.cs index 09b34abe..4285367a 100644 --- a/web/backend/Features/Features.Auth/Services/ITokenService.cs +++ b/web/backend/Features/Features.Auth/Services/ITokenService.cs @@ -15,7 +15,7 @@ public enum TokenType RefreshToken, /// A short-lived token used to confirm a user's email/account. - ConfirmationToken + ConfirmationToken, } /// @@ -32,11 +32,7 @@ public record ValidatedToken(Guid UserId, string Username, ClaimsPrincipal Princ /// The user account associated with the refreshed session. /// The newly issued refresh token. /// The newly issued access token. -public record RefreshTokenResult( - UserAccount UserAccount, - string RefreshToken, - string AccessToken -); +public record RefreshTokenResult(UserAccount UserAccount, string RefreshToken, string AccessToken); /// /// Defines the expiration windows, in hours, for each type of token issued by . @@ -85,7 +81,8 @@ public interface ITokenService /// The enum type identifying which kind of token to generate. Must be . /// The user account to generate the token for. /// The signed token string corresponding to the requested token type. - string GenerateToken(UserAccount user) where T : struct, Enum; + string GenerateToken(UserAccount user) + where T : struct, Enum; /// /// Validates an access token. @@ -114,4 +111,4 @@ public interface ITokenService /// The refresh token string to validate and exchange. /// A containing the user and newly issued tokens. Task RefreshTokenAsync(string refreshTokenString); -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Services/TokenService.cs b/web/backend/Features/Features.Auth/Services/TokenService.cs index e9b4a34f..1bbf13f2 100644 --- a/web/backend/Features/Features.Auth/Services/TokenService.cs +++ b/web/backend/Features/Features.Auth/Services/TokenService.cs @@ -29,25 +29,28 @@ public class TokenService : ITokenService /// Thrown when any of the ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET, or /// CONFIRMATION_TOKEN_SECRET environment variables are not set. /// - public TokenService( - ITokenInfrastructure tokenInfrastructure, - IAuthRepository authRepository - ) + public TokenService(ITokenInfrastructure tokenInfrastructure, IAuthRepository authRepository) { _tokenInfrastructure = tokenInfrastructure; _authRepository = authRepository; - _accessTokenSecret = Environment.GetEnvironmentVariable("ACCESS_TOKEN_SECRET") - ?? throw new InvalidOperationException( - "ACCESS_TOKEN_SECRET environment variable is not set"); + _accessTokenSecret = + Environment.GetEnvironmentVariable("ACCESS_TOKEN_SECRET") + ?? throw new InvalidOperationException( + "ACCESS_TOKEN_SECRET environment variable is not set" + ); - _refreshTokenSecret = Environment.GetEnvironmentVariable("REFRESH_TOKEN_SECRET") - ?? throw new InvalidOperationException( - "REFRESH_TOKEN_SECRET environment variable is not set"); + _refreshTokenSecret = + Environment.GetEnvironmentVariable("REFRESH_TOKEN_SECRET") + ?? throw new InvalidOperationException( + "REFRESH_TOKEN_SECRET environment variable is not set" + ); - _confirmationTokenSecret = Environment.GetEnvironmentVariable("CONFIRMATION_TOKEN_SECRET") - ?? throw new InvalidOperationException( - "CONFIRMATION_TOKEN_SECRET environment variable is not set"); + _confirmationTokenSecret = + Environment.GetEnvironmentVariable("CONFIRMATION_TOKEN_SECRET") + ?? throw new InvalidOperationException( + "CONFIRMATION_TOKEN_SECRET environment variable is not set" + ); } /// @@ -59,7 +62,12 @@ public class TokenService : ITokenService public string GenerateAccessToken(UserAccount user) { DateTime expiresAt = DateTime.UtcNow.AddHours(TokenServiceExpirationHours.AccessTokenHours); - return _tokenInfrastructure.GenerateJwt(user.UserAccountId, user.Username, expiresAt, _accessTokenSecret); + return _tokenInfrastructure.GenerateJwt( + user.UserAccountId, + user.Username, + expiresAt, + _accessTokenSecret + ); } /// @@ -70,8 +78,15 @@ public class TokenService : ITokenService /// The signed refresh token string. public string GenerateRefreshToken(UserAccount user) { - DateTime expiresAt = DateTime.UtcNow.AddHours(TokenServiceExpirationHours.RefreshTokenHours); - return _tokenInfrastructure.GenerateJwt(user.UserAccountId, user.Username, expiresAt, _refreshTokenSecret); + DateTime expiresAt = DateTime.UtcNow.AddHours( + TokenServiceExpirationHours.RefreshTokenHours + ); + return _tokenInfrastructure.GenerateJwt( + user.UserAccountId, + user.Username, + expiresAt, + _refreshTokenSecret + ); } /// @@ -82,8 +97,15 @@ public class TokenService : ITokenService /// The signed confirmation token string. public string GenerateConfirmationToken(UserAccount user) { - DateTime expiresAt = DateTime.UtcNow.AddHours(TokenServiceExpirationHours.ConfirmationTokenHours); - return _tokenInfrastructure.GenerateJwt(user.UserAccountId, user.Username, expiresAt, _confirmationTokenSecret); + DateTime expiresAt = DateTime.UtcNow.AddHours( + TokenServiceExpirationHours.ConfirmationTokenHours + ); + return _tokenInfrastructure.GenerateJwt( + user.UserAccountId, + user.Username, + expiresAt, + _confirmationTokenSecret + ); } /// @@ -97,7 +119,8 @@ public class TokenService : ITokenService /// /// Thrown when is not or does not resolve to a known token type. /// - public string GenerateToken(UserAccount user) where T : struct, Enum + public string GenerateToken(UserAccount user) + where T : struct, Enum { if (typeof(T) != typeof(TokenType)) throw new InvalidOperationException("Invalid token type"); @@ -112,7 +135,7 @@ public class TokenService : ITokenService TokenType.AccessToken => GenerateAccessToken(user), TokenType.RefreshToken => GenerateRefreshToken(user), TokenType.ConfirmationToken => GenerateConfirmationToken(user), - _ => throw new InvalidOperationException("Invalid token type") + _ => throw new InvalidOperationException("Invalid token type"), }; } @@ -189,7 +212,11 @@ public class TokenService : ITokenService /// Thrown when required claims are missing, the user ID claim is not a valid , /// or the underlying token validation fails for any other reason (e.g. expired or invalid signature). /// - private async Task ValidateTokenInternalAsync(string token, string secret, string tokenType) + private async Task ValidateTokenInternalAsync( + string token, + string secret, + string tokenType + ) { try { @@ -199,7 +226,9 @@ public class TokenService : ITokenService string? usernameClaim = principal.FindFirst(JwtRegisteredClaimNames.UniqueName)?.Value; if (string.IsNullOrEmpty(userIdClaim) || string.IsNullOrEmpty(usernameClaim)) - throw new UnauthorizedException($"Invalid {tokenType} token: missing required claims"); + throw new UnauthorizedException( + $"Invalid {tokenType} token: missing required claims" + ); if (!Guid.TryParse(userIdClaim, out Guid userId)) throw new UnauthorizedException($"Invalid {tokenType} token: malformed user ID"); @@ -215,4 +244,4 @@ public class TokenService : ITokenService throw new UnauthorizedException($"Failed to validate {tokenType} token: {e.Message}"); } } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Breweries.Tests/Commands/CreateBreweryHandlerTests.cs b/web/backend/Features/Features.Breweries.Tests/Commands/CreateBreweryHandlerTests.cs index 6bbf7b40..f1bfdb20 100644 --- a/web/backend/Features/Features.Breweries.Tests/Commands/CreateBreweryHandlerTests.cs +++ b/web/backend/Features/Features.Breweries.Tests/Commands/CreateBreweryHandlerTests.cs @@ -19,24 +19,13 @@ public class CreateBreweryHandlerTests private static CreateBreweryLocation ValidLocation() { - return new CreateBreweryLocation( - Guid.NewGuid(), - "123 Main St", - null, - "12345", - null - ); + return new CreateBreweryLocation(Guid.NewGuid(), "123 Main St", null, "12345", null); } [Fact] public async Task Handle_PersistsEntity_WithNewIdsAndCreatedAt() { - CreateBreweryCommand command = new( - Guid.NewGuid(), - "MyBrew", - "Desc", - ValidLocation() - ); + CreateBreweryCommand command = new(Guid.NewGuid(), "MyBrew", "Desc", ValidLocation()); BreweryPost? persisted = null; _repoMock @@ -68,4 +57,4 @@ public class CreateBreweryHandlerTests result.BreweryName.Should().Be("MyBrew"); result.Location.Should().NotBeNull(); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Breweries.Tests/Commands/DeleteBreweryHandlerTests.cs b/web/backend/Features/Features.Breweries.Tests/Commands/DeleteBreweryHandlerTests.cs index 243ca81d..39736f6a 100644 --- a/web/backend/Features/Features.Breweries.Tests/Commands/DeleteBreweryHandlerTests.cs +++ b/web/backend/Features/Features.Breweries.Tests/Commands/DeleteBreweryHandlerTests.cs @@ -18,4 +18,4 @@ public class DeleteBreweryHandlerTests repoMock.Verify(r => r.DeleteAsync(id), Times.Once); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Breweries.Tests/Commands/UpdateBreweryHandlerTests.cs b/web/backend/Features/Features.Breweries.Tests/Commands/UpdateBreweryHandlerTests.cs index 02996126..c0871984 100644 --- a/web/backend/Features/Features.Breweries.Tests/Commands/UpdateBreweryHandlerTests.cs +++ b/web/backend/Features/Features.Breweries.Tests/Commands/UpdateBreweryHandlerTests.cs @@ -96,11 +96,13 @@ public class UpdateBreweryHandlerTests await _handler.Handle(command, CancellationToken.None); persisted!.Location.Should().NotBeNull(); - persisted.Location!.BreweryPostLocationId.Should().Be(locationCommand.BreweryPostLocationId); + 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); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj b/web/backend/Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj index 7ddeb83b..e97da606 100644 --- a/web/backend/Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj +++ b/web/backend/Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj @@ -8,19 +8,19 @@ - - - - - - + + + + + + - + - + diff --git a/web/backend/Features/Features.Breweries.Tests/Queries/GetAllBreweriesHandlerTests.cs b/web/backend/Features/Features.Breweries.Tests/Queries/GetAllBreweriesHandlerTests.cs index 5a0478db..32a4459c 100644 --- a/web/backend/Features/Features.Breweries.Tests/Queries/GetAllBreweriesHandlerTests.cs +++ b/web/backend/Features/Features.Breweries.Tests/Queries/GetAllBreweriesHandlerTests.cs @@ -20,10 +20,12 @@ public class GetAllBreweriesHandlerTests [Fact] public async Task Handle_PassesLimitAndOffset_ToRepository() { - _repoMock.Setup(r => r.GetAllAsync(10, 5)) - .ReturnsAsync(Array.Empty()); + _repoMock.Setup(r => r.GetAllAsync(10, 5)).ReturnsAsync(Array.Empty()); - IEnumerable result = await _handler.Handle(new GetAllBreweriesQuery(10, 5), CancellationToken.None); + IEnumerable result = await _handler.Handle( + new GetAllBreweriesQuery(10, 5), + CancellationToken.None + ); result.Should().BeEmpty(); _repoMock.Verify(r => r.GetAllAsync(10, 5), Times.Once); @@ -35,13 +37,18 @@ public class GetAllBreweriesHandlerTests BreweryPost[] breweries = new[] { new BreweryPost { BreweryPostId = Guid.NewGuid(), BreweryName = "A" }, - new BreweryPost { BreweryPostId = Guid.NewGuid(), BreweryName = "B" } + new BreweryPost { BreweryPostId = Guid.NewGuid(), BreweryName = "B" }, }; - _repoMock.Setup(r => r.GetAllAsync(null, null)) - .ReturnsAsync(breweries); + _repoMock.Setup(r => r.GetAllAsync(null, null)).ReturnsAsync(breweries); - IEnumerable result = await _handler.Handle(new GetAllBreweriesQuery(null, null), CancellationToken.None); + IEnumerable result = await _handler.Handle( + new GetAllBreweriesQuery(null, null), + CancellationToken.None + ); - result.Select(b => b.BreweryPostId).Should().BeEquivalentTo(breweries.Select(b => b.BreweryPostId)); + result + .Select(b => b.BreweryPostId) + .Should() + .BeEquivalentTo(breweries.Select(b => b.BreweryPostId)); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Breweries.Tests/Queries/GetBreweryByIdHandlerTests.cs b/web/backend/Features/Features.Breweries.Tests/Queries/GetBreweryByIdHandlerTests.cs index be453eff..de29911b 100644 --- a/web/backend/Features/Features.Breweries.Tests/Queries/GetBreweryByIdHandlerTests.cs +++ b/web/backend/Features/Features.Breweries.Tests/Queries/GetBreweryByIdHandlerTests.cs @@ -21,10 +21,12 @@ public class GetBreweryByIdHandlerTests public async Task Handle_ReturnsBrewery_WhenFound() { BreweryPost brewery = new() { BreweryPostId = Guid.NewGuid(), BreweryName = "Test" }; - _repoMock.Setup(r => r.GetByIdAsync(brewery.BreweryPostId)) - .ReturnsAsync(brewery); + _repoMock.Setup(r => r.GetByIdAsync(brewery.BreweryPostId)).ReturnsAsync(brewery); - BreweryDto? result = await _handler.Handle(new GetBreweryByIdQuery(brewery.BreweryPostId), CancellationToken.None); + BreweryDto? result = await _handler.Handle( + new GetBreweryByIdQuery(brewery.BreweryPostId), + CancellationToken.None + ); result.Should().NotBeNull(); result!.BreweryPostId.Should().Be(brewery.BreweryPostId); @@ -34,11 +36,13 @@ public class GetBreweryByIdHandlerTests public async Task Handle_ReturnsNull_WhenNotFound() { Guid id = Guid.NewGuid(); - _repoMock.Setup(r => r.GetByIdAsync(id)) - .ReturnsAsync((BreweryPost?)null); + _repoMock.Setup(r => r.GetByIdAsync(id)).ReturnsAsync((BreweryPost?)null); - BreweryDto? result = await _handler.Handle(new GetBreweryByIdQuery(id), CancellationToken.None); + BreweryDto? result = await _handler.Handle( + new GetBreweryByIdQuery(id), + CancellationToken.None + ); result.Should().BeNull(); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Breweries.Tests/Repository/BreweryRepositoryTests.cs b/web/backend/Features/Features.Breweries.Tests/Repository/BreweryRepositoryTests.cs index 57395642..eaffef0c 100644 --- a/web/backend/Features/Features.Breweries.Tests/Repository/BreweryRepositoryTests.cs +++ b/web/backend/Features/Features.Breweries.Tests/Repository/BreweryRepositoryTests.cs @@ -81,8 +81,7 @@ public class BreweryRepositoryTests public async Task CreateAsync_ExecutesSuccessfully() { MockDbConnection conn = new(); - conn.Mocks.When(cmd => cmd.CommandText == "USP_CreateBrewery") - .ReturnsScalar(1); + conn.Mocks.When(cmd => cmd.CommandText == "USP_CreateBrewery").ReturnsScalar(1); BreweryRepository repo = CreateRepo(conn); BreweryPost brewery = new() { @@ -97,12 +96,12 @@ public class BreweryRepositoryTests CityId = Guid.NewGuid(), AddressLine1 = "123 Main St", PostalCode = "12345", - Coordinates = [0x00, 0x01] - } + Coordinates = [0x00, 0x01], + }, }; // Should not throw Func act = async () => await repo.CreateAsync(brewery); await act.Should().NotThrowAsync(); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Breweries.Tests/Repository/TestConnectionFactory.cs b/web/backend/Features/Features.Breweries.Tests/Repository/TestConnectionFactory.cs index 9ebb3fdd..8ab7808f 100644 --- a/web/backend/Features/Features.Breweries.Tests/Repository/TestConnectionFactory.cs +++ b/web/backend/Features/Features.Breweries.Tests/Repository/TestConnectionFactory.cs @@ -11,4 +11,4 @@ internal class TestConnectionFactory(DbConnection conn) : ISqlConnectionFactory { return _conn; } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryCommand.cs b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryCommand.cs index 97e7cd84..a06dadc3 100644 --- a/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryCommand.cs +++ b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryCommand.cs @@ -22,4 +22,4 @@ public record CreateBreweryCommand( string BreweryName, string Description, CreateBreweryLocation Location -) : IRequest; \ No newline at end of file +) : IRequest; diff --git a/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryHandler.cs b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryHandler.cs index 26073a8c..07c0f1d3 100644 --- a/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryHandler.cs +++ b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryHandler.cs @@ -18,7 +18,10 @@ public class CreateBreweryHandler(IBreweryRepository repository) /// The details of the brewery post to create. /// A token to observe for cancellation requests. /// The newly created brewery post. - public async Task Handle(CreateBreweryCommand request, CancellationToken cancellationToken) + public async Task Handle( + CreateBreweryCommand request, + CancellationToken cancellationToken + ) { BreweryPost entity = new() { @@ -34,11 +37,11 @@ public class CreateBreweryHandler(IBreweryRepository repository) AddressLine1 = request.Location.AddressLine1, AddressLine2 = request.Location.AddressLine2, PostalCode = request.Location.PostalCode, - Coordinates = request.Location.Coordinates - } + Coordinates = request.Location.Coordinates, + }, }; await repository.CreateAsync(entity); return entity.ToDto(); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryValidator.cs b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryValidator.cs index 3d70d2d2..ad5f528f 100644 --- a/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryValidator.cs +++ b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryValidator.cs @@ -15,9 +15,7 @@ public class CreateBreweryValidator : AbstractValidator /// public CreateBreweryValidator() { - RuleFor(x => x.PostedById) - .NotEmpty() - .WithMessage("PostedById is required."); + RuleFor(x => x.PostedById).NotEmpty().WithMessage("PostedById is required."); RuleFor(x => x.BreweryName) .NotEmpty() @@ -31,9 +29,7 @@ public class CreateBreweryValidator : AbstractValidator .MaximumLength(512) .WithMessage("Description cannot exceed 512 characters."); - RuleFor(x => x.Location) - .NotNull() - .WithMessage("Location is required."); + RuleFor(x => x.Location).NotNull().WithMessage("Location is required."); RuleFor(x => x.Location.CityId) .NotEmpty() @@ -56,4 +52,4 @@ public class CreateBreweryValidator : AbstractValidator .When(x => x.Location is not null) .WithMessage("Postal code cannot exceed 20 characters."); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryCommand.cs b/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryCommand.cs index da74352d..2b07ec6f 100644 --- a/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryCommand.cs +++ b/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryCommand.cs @@ -5,4 +5,4 @@ namespace Features.Breweries.Commands.DeleteBrewery; /// /// Deletes a brewery post by its unique identifier. /// -public record DeleteBreweryCommand(Guid BreweryPostId) : IRequest; \ No newline at end of file +public record DeleteBreweryCommand(Guid BreweryPostId) : IRequest; diff --git a/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryHandler.cs b/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryHandler.cs index 41eabdee..226a0d04 100644 --- a/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryHandler.cs +++ b/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryHandler.cs @@ -14,4 +14,4 @@ public class DeleteBreweryHandler(IBreweryRepository repository) { return repository.DeleteAsync(request.BreweryPostId); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryCommand.cs b/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryCommand.cs index 47ac0c10..f2dc90db 100644 --- a/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryCommand.cs +++ b/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryCommand.cs @@ -25,4 +25,4 @@ public record UpdateBreweryCommand( string BreweryName, string Description, UpdateBreweryLocation? Location -) : IRequest; \ No newline at end of file +) : IRequest; diff --git a/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryHandler.cs b/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryHandler.cs index 9462bd33..cb6c033a 100644 --- a/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryHandler.cs +++ b/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryHandler.cs @@ -19,7 +19,10 @@ public class UpdateBreweryHandler(IBreweryRepository repository) /// The updated details of the brewery post. /// A token to observe for cancellation requests. /// The updated brewery post. - public async Task Handle(UpdateBreweryCommand request, CancellationToken cancellationToken) + public async Task Handle( + UpdateBreweryCommand request, + CancellationToken cancellationToken + ) { BreweryPost entity = new() { @@ -38,11 +41,11 @@ public class UpdateBreweryHandler(IBreweryRepository repository) AddressLine1 = request.Location.AddressLine1, AddressLine2 = request.Location.AddressLine2, PostalCode = request.Location.PostalCode, - Coordinates = request.Location.Coordinates - } + Coordinates = request.Location.Coordinates, + }, }; await repository.UpdateAsync(entity); return entity.ToDto(); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Breweries/Controllers/BreweryController.cs b/web/backend/Features/Features.Breweries/Controllers/BreweryController.cs index bdff8fc6..25eb393c 100644 --- a/web/backend/Features/Features.Breweries/Controllers/BreweryController.cs +++ b/web/backend/Features/Features.Breweries/Controllers/BreweryController.cs @@ -41,11 +41,13 @@ public class BreweryController(IMediator mediator) : ControllerBase if (brewery is null) return NotFound(new ResponseBody { Message = $"Brewery with ID {id} not found." }); - return Ok(new ResponseBody - { - Message = "Brewery retrieved successfully.", - Payload = brewery - }); + return Ok( + new ResponseBody + { + Message = "Brewery retrieved successfully.", + Payload = brewery, + } + ); } /// @@ -59,14 +61,19 @@ public class BreweryController(IMediator mediator) : ControllerBase [HttpGet] public async Task>>> GetAll( [FromQuery] int? limit, - [FromQuery] int? offset) + [FromQuery] int? offset + ) { - IEnumerable breweries = await mediator.Send(new GetAllBreweriesQuery(limit, offset)); - return Ok(new ResponseBody> - { - Message = "Breweries retrieved successfully.", - Payload = breweries - }); + IEnumerable breweries = await mediator.Send( + new GetAllBreweriesQuery(limit, offset) + ); + return Ok( + new ResponseBody> + { + Message = "Breweries retrieved successfully.", + Payload = breweries, + } + ); } /// @@ -78,14 +85,19 @@ public class BreweryController(IMediator mediator) : ControllerBase /// . /// [HttpPost] - public async Task>> Create([FromBody] CreateBreweryCommand command) + public async Task>> Create( + [FromBody] CreateBreweryCommand command + ) { BreweryDto brewery = await mediator.Send(command); - return Created($"/api/brewery/{brewery.BreweryPostId}", new ResponseBody - { - Message = "Brewery created successfully.", - Payload = brewery - }); + return Created( + $"/api/brewery/{brewery.BreweryPostId}", + new ResponseBody + { + Message = "Brewery created successfully.", + Payload = brewery, + } + ); } /// @@ -105,17 +117,22 @@ public class BreweryController(IMediator mediator) : ControllerBase /// when the route ID does not match the payload ID. /// [HttpPut("{id:guid}")] - public async Task>> Update(Guid id, [FromBody] UpdateBreweryCommand command) + public async Task>> Update( + Guid id, + [FromBody] UpdateBreweryCommand command + ) { if (command.BreweryPostId != id) return BadRequest(new ResponseBody { Message = "Route ID does not match payload ID." }); BreweryDto brewery = await mediator.Send(command); - return Ok(new ResponseBody - { - Message = "Brewery updated successfully.", - Payload = brewery - }); + return Ok( + new ResponseBody + { + Message = "Brewery updated successfully.", + Payload = brewery, + } + ); } /// @@ -129,4 +146,4 @@ public class BreweryController(IMediator mediator) : ControllerBase await mediator.Send(new DeleteBreweryCommand(id)); return Ok(new ResponseBody { Message = "Brewery deleted successfully." }); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Breweries/DependencyInjection/FeaturesBreweriesServiceCollectionExtensions.cs b/web/backend/Features/Features.Breweries/DependencyInjection/FeaturesBreweriesServiceCollectionExtensions.cs index 957a7e60..b4707c78 100644 --- a/web/backend/Features/Features.Breweries/DependencyInjection/FeaturesBreweriesServiceCollectionExtensions.cs +++ b/web/backend/Features/Features.Breweries/DependencyInjection/FeaturesBreweriesServiceCollectionExtensions.cs @@ -13,4 +13,4 @@ public static class FeaturesBreweriesServiceCollectionExtensions services.AddScoped(); return services; } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Breweries/Dtos/BreweryDto.cs b/web/backend/Features/Features.Breweries/Dtos/BreweryDto.cs index 41867736..616cacf4 100644 --- a/web/backend/Features/Features.Breweries/Dtos/BreweryDto.cs +++ b/web/backend/Features/Features.Breweries/Dtos/BreweryDto.cs @@ -86,4 +86,4 @@ public class BreweryDto /// The location details of the brewery, or null if no location is associated with it. /// public BreweryLocationDto? Location { get; set; } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Breweries/Dtos/BreweryDtoMapper.cs b/web/backend/Features/Features.Breweries/Dtos/BreweryDtoMapper.cs index baeef350..4cba3518 100644 --- a/web/backend/Features/Features.Breweries/Dtos/BreweryDtoMapper.cs +++ b/web/backend/Features/Features.Breweries/Dtos/BreweryDtoMapper.cs @@ -34,8 +34,8 @@ public static class BreweryDtoMapper AddressLine1 = brewery.Location.AddressLine1, AddressLine2 = brewery.Location.AddressLine2, PostalCode = brewery.Location.PostalCode, - Coordinates = brewery.Location.Coordinates - } + Coordinates = brewery.Location.Coordinates, + }, }; } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Breweries/Features.Breweries.csproj b/web/backend/Features/Features.Breweries/Features.Breweries.csproj index d83006ab..93645162 100644 --- a/web/backend/Features/Features.Breweries/Features.Breweries.csproj +++ b/web/backend/Features/Features.Breweries/Features.Breweries.csproj @@ -7,13 +7,13 @@ - + - - - - + + + + diff --git a/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesHandler.cs b/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesHandler.cs index 314d7a91..e9a37484 100644 --- a/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesHandler.cs +++ b/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesHandler.cs @@ -12,9 +12,15 @@ namespace Features.Breweries.Queries.GetAllBreweries; public class GetAllBreweriesHandler(IBreweryRepository repository) : IRequestHandler> { - public async Task> Handle(GetAllBreweriesQuery request, CancellationToken cancellationToken) + public async Task> Handle( + GetAllBreweriesQuery request, + CancellationToken cancellationToken + ) { - IEnumerable breweries = await repository.GetAllAsync(request.Limit, request.Offset); + IEnumerable breweries = await repository.GetAllAsync( + request.Limit, + request.Offset + ); return breweries.Select(b => b.ToDto()); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesQuery.cs b/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesQuery.cs index ef3c437e..4fe7af88 100644 --- a/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesQuery.cs +++ b/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesQuery.cs @@ -6,4 +6,4 @@ namespace Features.Breweries.Queries.GetAllBreweries; /// /// Retrieves a paginated list of brewery posts. /// -public record GetAllBreweriesQuery(int? Limit, int? Offset) : IRequest>; \ No newline at end of file +public record GetAllBreweriesQuery(int? Limit, int? Offset) : IRequest>; diff --git a/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdHandler.cs b/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdHandler.cs index 0ad63b9b..72f510e1 100644 --- a/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdHandler.cs +++ b/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdHandler.cs @@ -12,9 +12,12 @@ namespace Features.Breweries.Queries.GetBreweryById; public class GetBreweryByIdHandler(IBreweryRepository repository) : IRequestHandler { - public async Task Handle(GetBreweryByIdQuery request, CancellationToken cancellationToken) + public async Task Handle( + GetBreweryByIdQuery request, + CancellationToken cancellationToken + ) { BreweryPost? brewery = await repository.GetByIdAsync(request.BreweryPostId); return brewery?.ToDto(); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdQuery.cs b/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdQuery.cs index 7c259a14..5f4fc13f 100644 --- a/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdQuery.cs +++ b/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdQuery.cs @@ -6,4 +6,4 @@ namespace Features.Breweries.Queries.GetBreweryById; /// /// Retrieves a single brewery post by its unique identifier. /// -public record GetBreweryByIdQuery(Guid BreweryPostId) : IRequest; \ No newline at end of file +public record GetBreweryByIdQuery(Guid BreweryPostId) : IRequest; diff --git a/web/backend/Features/Features.Breweries/Repository/BreweryRepository.cs b/web/backend/Features/Features.Breweries/Repository/BreweryRepository.cs index 71523e25..cc5e24b6 100644 --- a/web/backend/Features/Features.Breweries/Repository/BreweryRepository.cs +++ b/web/backend/Features/Features.Breweries/Repository/BreweryRepository.cs @@ -11,7 +11,8 @@ namespace Features.Breweries.Repository; /// /// The factory used to create database connections. public class BreweryRepository(ISqlConnectionFactory connectionFactory) - : Repository(connectionFactory), IBreweryRepository + : Repository(connectionFactory), + IBreweryRepository { /// /// Retrieves a brewery post by ID using the USP_GetBreweryById stored procedure. @@ -29,7 +30,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory) AddParameter(command, "@BreweryPostID", id); await using DbDataReader reader = await command.ExecuteReaderAsync(); - if (await reader.ReadAsync()) return MapToEntity(reader); + if (await reader.ReadAsync()) + return MapToEntity(reader); return null; } @@ -58,7 +60,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory) await using DbDataReader reader = await command.ExecuteReaderAsync(); List breweries = new(); - while (await reader.ReadAsync()) breweries.Add(MapToEntity(reader)); + while (await reader.ReadAsync()) + breweries.Add(MapToEntity(reader)); return breweries; } @@ -121,7 +124,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory) command.CommandText = "USP_CreateBrewery"; command.CommandType = CommandType.StoredProcedure; - if (brewery.Location is null) throw new ArgumentException("Location must be provided when creating a brewery."); + 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); @@ -195,7 +199,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory) PostalCode = reader.GetString(reader.GetOrdinal("PostalCode")), Coordinates = reader.IsDBNull(reader.GetOrdinal("Coordinates")) ? null - : reader.GetFieldValue(reader.GetOrdinal("Coordinates")) + : reader.GetFieldValue(reader.GetOrdinal("Coordinates")), }; brewery.Location = location; } @@ -215,15 +219,11 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory) /// The command to add the parameter to. /// The parameter name (including any prefix, e.g. "@BreweryName"). /// The parameter value, or null to bind . - private static void AddParameter( - DbCommand command, - string name, - object? value - ) + private static void AddParameter(DbCommand command, string name, object? value) { DbParameter p = command.CreateParameter(); p.ParameterName = name; p.Value = value ?? DBNull.Value; command.Parameters.Add(p); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Breweries/Repository/IBreweryRepository.cs b/web/backend/Features/Features.Breweries/Repository/IBreweryRepository.cs index d0a0972e..e976c2ad 100644 --- a/web/backend/Features/Features.Breweries/Repository/IBreweryRepository.cs +++ b/web/backend/Features/Features.Breweries/Repository/IBreweryRepository.cs @@ -40,4 +40,4 @@ public interface IBreweryRepository /// The brewery post to create. Must have a non-null Location. /// Thrown when has no Location. Task CreateAsync(BreweryPost brewery); -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Emails.Tests/Commands/SendRegistrationEmailHandlerTests.cs b/web/backend/Features/Features.Emails.Tests/Commands/SendRegistrationEmailHandlerTests.cs index 4e91e62d..9b338b9a 100644 --- a/web/backend/Features/Features.Emails.Tests/Commands/SendRegistrationEmailHandlerTests.cs +++ b/web/backend/Features/Features.Emails.Tests/Commands/SendRegistrationEmailHandlerTests.cs @@ -21,4 +21,4 @@ public class SendRegistrationEmailHandlerTests Times.Once ); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Emails.Tests/Commands/SendResendConfirmationEmailHandlerTests.cs b/web/backend/Features/Features.Emails.Tests/Commands/SendResendConfirmationEmailHandlerTests.cs index 1563013a..b8b02f65 100644 --- a/web/backend/Features/Features.Emails.Tests/Commands/SendResendConfirmationEmailHandlerTests.cs +++ b/web/backend/Features/Features.Emails.Tests/Commands/SendResendConfirmationEmailHandlerTests.cs @@ -21,4 +21,4 @@ public class SendResendConfirmationEmailHandlerTests Times.Once ); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Emails.Tests/Features.Emails.Tests.csproj b/web/backend/Features/Features.Emails.Tests/Features.Emails.Tests.csproj index 3234d955..11921d56 100644 --- a/web/backend/Features/Features.Emails.Tests/Features.Emails.Tests.csproj +++ b/web/backend/Features/Features.Emails.Tests/Features.Emails.Tests.csproj @@ -8,18 +8,18 @@ - - - - - + + + + + - + - + diff --git a/web/backend/Features/Features.Emails/Commands/SendRegistrationEmail/SendRegistrationEmailHandler.cs b/web/backend/Features/Features.Emails/Commands/SendRegistrationEmail/SendRegistrationEmailHandler.cs index b840a455..0da0cdce 100644 --- a/web/backend/Features/Features.Emails/Commands/SendRegistrationEmail/SendRegistrationEmailHandler.cs +++ b/web/backend/Features/Features.Emails/Commands/SendRegistrationEmail/SendRegistrationEmailHandler.cs @@ -14,6 +14,10 @@ public class SendRegistrationEmailHandler(IEmailDispatcher emailDispatcher) { public Task Handle(SendRegistrationEmailCommand request, CancellationToken cancellationToken) { - return emailDispatcher.SendRegistrationEmailAsync(request.FirstName, request.Email, request.ConfirmationToken); + return emailDispatcher.SendRegistrationEmailAsync( + request.FirstName, + request.Email, + request.ConfirmationToken + ); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Emails/Commands/SendResendConfirmationEmail/SendResendConfirmationEmailHandler.cs b/web/backend/Features/Features.Emails/Commands/SendResendConfirmationEmail/SendResendConfirmationEmailHandler.cs index d11f1d07..17f52009 100644 --- a/web/backend/Features/Features.Emails/Commands/SendResendConfirmationEmail/SendResendConfirmationEmailHandler.cs +++ b/web/backend/Features/Features.Emails/Commands/SendResendConfirmationEmail/SendResendConfirmationEmailHandler.cs @@ -12,9 +12,15 @@ namespace Features.Emails.Commands.SendResendConfirmationEmail; public class SendResendConfirmationEmailHandler(IEmailDispatcher emailDispatcher) : IRequestHandler { - public Task Handle(SendResendConfirmationEmailCommand request, CancellationToken cancellationToken) + public Task Handle( + SendResendConfirmationEmailCommand request, + CancellationToken cancellationToken + ) { - return emailDispatcher.SendResendConfirmationEmailAsync(request.FirstName, request.Email, - request.ConfirmationToken); + return emailDispatcher.SendResendConfirmationEmailAsync( + request.FirstName, + request.Email, + request.ConfirmationToken + ); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Emails/DependencyInjection/FeaturesEmailsServiceCollectionExtensions.cs b/web/backend/Features/Features.Emails/DependencyInjection/FeaturesEmailsServiceCollectionExtensions.cs index f5ac6be0..8b97dbdb 100644 --- a/web/backend/Features/Features.Emails/DependencyInjection/FeaturesEmailsServiceCollectionExtensions.cs +++ b/web/backend/Features/Features.Emails/DependencyInjection/FeaturesEmailsServiceCollectionExtensions.cs @@ -17,4 +17,4 @@ public static class FeaturesEmailsServiceCollectionExtensions services.AddScoped(); return services; } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Emails/Features.Emails.csproj b/web/backend/Features/Features.Emails/Features.Emails.csproj index cf71399d..2f261aa6 100644 --- a/web/backend/Features/Features.Emails/Features.Emails.csproj +++ b/web/backend/Features/Features.Emails/Features.Emails.csproj @@ -7,8 +7,8 @@ - - - + + + diff --git a/web/backend/Features/Features.Emails/Services/EmailDispatcher.cs b/web/backend/Features/Features.Emails/Services/EmailDispatcher.cs index 54ce9720..436560e6 100644 --- a/web/backend/Features/Features.Emails/Services/EmailDispatcher.cs +++ b/web/backend/Features/Features.Emails/Services/EmailDispatcher.cs @@ -29,39 +29,38 @@ public class EmailDispatcher( /// Builds a confirmation link from the given token, renders the registration welcome email template, /// and sends it to the newly created user. /// - public async Task SendRegistrationEmailAsync(string firstName, string email, string confirmationToken) + public async Task SendRegistrationEmailAsync( + string firstName, + string email, + string confirmationToken + ) { - string confirmationLink = - $"{WebsiteBaseUrl}/users/confirm?token={confirmationToken}"; + string confirmationLink = $"{WebsiteBaseUrl}/users/confirm?token={confirmationToken}"; - string emailHtml = - await emailTemplateProvider.RenderUserRegisteredEmailAsync( - firstName, - confirmationLink - ); - - await emailProvider.SendAsync( - email, - "Welcome to The Biergarten App!", - emailHtml, - true + string emailHtml = await emailTemplateProvider.RenderUserRegisteredEmailAsync( + firstName, + confirmationLink ); + + await emailProvider.SendAsync(email, "Welcome to The Biergarten App!", emailHtml, true); } /// /// Builds a confirmation link from the given token, renders the resend-confirmation email template, /// and sends it to the user. /// - public async Task SendResendConfirmationEmailAsync(string firstName, string email, string confirmationToken) + public async Task SendResendConfirmationEmailAsync( + string firstName, + string email, + string confirmationToken + ) { - string confirmationLink = - $"{WebsiteBaseUrl}/users/confirm?token={confirmationToken}"; + string confirmationLink = $"{WebsiteBaseUrl}/users/confirm?token={confirmationToken}"; - string emailHtml = - await emailTemplateProvider.RenderResendConfirmationEmailAsync( - firstName, - confirmationLink - ); + string emailHtml = await emailTemplateProvider.RenderResendConfirmationEmailAsync( + firstName, + confirmationLink + ); await emailProvider.SendAsync( email, @@ -70,4 +69,4 @@ public class EmailDispatcher( true ); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Emails/Services/IEmailDispatcher.cs b/web/backend/Features/Features.Emails/Services/IEmailDispatcher.cs index 7abe1b57..8a4b3267 100644 --- a/web/backend/Features/Features.Emails/Services/IEmailDispatcher.cs +++ b/web/backend/Features/Features.Emails/Services/IEmailDispatcher.cs @@ -22,4 +22,4 @@ public interface IEmailDispatcher /// The confirmation token to embed in the confirmation link. /// A task that completes once the email has been sent. Task SendResendConfirmationEmailAsync(string firstName, string email, string confirmationToken); -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.UserManagement.Tests/Commands/UpdateUserHandlerTests.cs b/web/backend/Features/Features.UserManagement.Tests/Commands/UpdateUserHandlerTests.cs index 66ce8132..53f3a800 100644 --- a/web/backend/Features/Features.UserManagement.Tests/Commands/UpdateUserHandlerTests.cs +++ b/web/backend/Features/Features.UserManagement.Tests/Commands/UpdateUserHandlerTests.cs @@ -19,4 +19,4 @@ public class UpdateUserHandlerTests repoMock.Verify(r => r.UpdateAsync(user), Times.Once); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.UserManagement.Tests/Features.UserManagement.Tests.csproj b/web/backend/Features/Features.UserManagement.Tests/Features.UserManagement.Tests.csproj index 3813b431..b5d6a61d 100644 --- a/web/backend/Features/Features.UserManagement.Tests/Features.UserManagement.Tests.csproj +++ b/web/backend/Features/Features.UserManagement.Tests/Features.UserManagement.Tests.csproj @@ -8,19 +8,19 @@ - - - - - - + + + + + + - + - + diff --git a/web/backend/Features/Features.UserManagement.Tests/Queries/GetAllUsersHandlerTests.cs b/web/backend/Features/Features.UserManagement.Tests/Queries/GetAllUsersHandlerTests.cs index 95055152..83b42d1c 100644 --- a/web/backend/Features/Features.UserManagement.Tests/Queries/GetAllUsersHandlerTests.cs +++ b/web/backend/Features/Features.UserManagement.Tests/Queries/GetAllUsersHandlerTests.cs @@ -15,9 +15,12 @@ public class GetAllUsersHandlerTests GetAllUsersHandler handler = new(repoMock.Object); repoMock.Setup(r => r.GetAllAsync(10, 5)).ReturnsAsync(Array.Empty()); - IEnumerable result = await handler.Handle(new GetAllUsersQuery(10, 5), CancellationToken.None); + IEnumerable result = await handler.Handle( + new GetAllUsersQuery(10, 5), + CancellationToken.None + ); result.Should().BeEmpty(); repoMock.Verify(r => r.GetAllAsync(10, 5), Times.Once); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.UserManagement.Tests/Queries/GetUserByIdHandlerTests.cs b/web/backend/Features/Features.UserManagement.Tests/Queries/GetUserByIdHandlerTests.cs index 23ca061a..e79b05b4 100644 --- a/web/backend/Features/Features.UserManagement.Tests/Queries/GetUserByIdHandlerTests.cs +++ b/web/backend/Features/Features.UserManagement.Tests/Queries/GetUserByIdHandlerTests.cs @@ -23,7 +23,10 @@ public class GetUserByIdHandlerTests UserAccount user = new() { UserAccountId = Guid.NewGuid(), Username = "test" }; _repoMock.Setup(r => r.GetByIdAsync(user.UserAccountId)).ReturnsAsync(user); - UserAccount result = await _handler.Handle(new GetUserByIdQuery(user.UserAccountId), CancellationToken.None); + UserAccount result = await _handler.Handle( + new GetUserByIdQuery(user.UserAccountId), + CancellationToken.None + ); result.Should().Be(user); } @@ -34,8 +37,9 @@ public class GetUserByIdHandlerTests Guid id = Guid.NewGuid(); _repoMock.Setup(r => r.GetByIdAsync(id)).ReturnsAsync((UserAccount?)null); - Func> act = async () => await _handler.Handle(new GetUserByIdQuery(id), CancellationToken.None); + Func> act = async () => + await _handler.Handle(new GetUserByIdQuery(id), CancellationToken.None); await act.Should().ThrowAsync(); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.UserManagement.Tests/Repository/TestConnectionFactory.cs b/web/backend/Features/Features.UserManagement.Tests/Repository/TestConnectionFactory.cs index 6454219f..d0a4c5a7 100644 --- a/web/backend/Features/Features.UserManagement.Tests/Repository/TestConnectionFactory.cs +++ b/web/backend/Features/Features.UserManagement.Tests/Repository/TestConnectionFactory.cs @@ -11,4 +11,4 @@ internal class TestConnectionFactory(DbConnection conn) : ISqlConnectionFactory { return _conn; } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.UserManagement.Tests/Repository/UserAccountRepositoryTests.cs b/web/backend/Features/Features.UserManagement.Tests/Repository/UserAccountRepositoryTests.cs index d38bc33c..5f26ec2f 100644 --- a/web/backend/Features/Features.UserManagement.Tests/Repository/UserAccountRepositoryTests.cs +++ b/web/backend/Features/Features.UserManagement.Tests/Repository/UserAccountRepositoryTests.cs @@ -98,19 +98,14 @@ public class UserAccountRepositoryTests UserAccountRepository repo = CreateRepo(conn); List results = (await repo.GetAllAsync(null, null)).ToList(); results.Should().HaveCount(2); - results - .Select(r => r.Username) - .Should() - .BeEquivalentTo("a", "b"); + results.Select(r => r.Username).Should().BeEquivalentTo("a", "b"); } [Fact] public async Task GetByUsername_ReturnsRow() { MockDbConnection conn = new(); - conn.Mocks.When(cmd => - cmd.CommandText == "usp_GetUserAccountByUsername" - ) + conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername") .ReturnsTable( MockTable .WithColumns( @@ -179,4 +174,4 @@ public class UserAccountRepositoryTests result.Should().NotBeNull(); result!.Username.Should().Be("byemail"); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.UserManagement/Commands/UpdateUser/UpdateUserCommand.cs b/web/backend/Features/Features.UserManagement/Commands/UpdateUser/UpdateUserCommand.cs index 4cc4b855..73d9c503 100644 --- a/web/backend/Features/Features.UserManagement/Commands/UpdateUser/UpdateUserCommand.cs +++ b/web/backend/Features/Features.UserManagement/Commands/UpdateUser/UpdateUserCommand.cs @@ -7,4 +7,4 @@ namespace Features.UserManagement.Commands.UpdateUser; /// Updates an existing user account. Not currently exposed via any HTTP route, carried forward /// from IUserService.UpdateAsync as-is. /// -public record UpdateUserCommand(UserAccount UserAccount) : IRequest; \ No newline at end of file +public record UpdateUserCommand(UserAccount UserAccount) : IRequest; diff --git a/web/backend/Features/Features.UserManagement/Commands/UpdateUser/UpdateUserHandler.cs b/web/backend/Features/Features.UserManagement/Commands/UpdateUser/UpdateUserHandler.cs index a6188b06..cee83da1 100644 --- a/web/backend/Features/Features.UserManagement/Commands/UpdateUser/UpdateUserHandler.cs +++ b/web/backend/Features/Features.UserManagement/Commands/UpdateUser/UpdateUserHandler.cs @@ -14,4 +14,4 @@ public class UpdateUserHandler(IUserAccountRepository repository) { return repository.UpdateAsync(request.UserAccount); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.UserManagement/Controllers/UserController.cs b/web/backend/Features/Features.UserManagement/Controllers/UserController.cs index 017834fb..90987d65 100644 --- a/web/backend/Features/Features.UserManagement/Controllers/UserController.cs +++ b/web/backend/Features/Features.UserManagement/Controllers/UserController.cs @@ -41,4 +41,4 @@ public class UserController(IMediator mediator) : ControllerBase UserAccount user = await mediator.Send(new GetUserByIdQuery(id)); return Ok(user); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.UserManagement/DependencyInjection/FeaturesUserManagementServiceCollectionExtensions.cs b/web/backend/Features/Features.UserManagement/DependencyInjection/FeaturesUserManagementServiceCollectionExtensions.cs index aaf9e97d..1c6c7947 100644 --- a/web/backend/Features/Features.UserManagement/DependencyInjection/FeaturesUserManagementServiceCollectionExtensions.cs +++ b/web/backend/Features/Features.UserManagement/DependencyInjection/FeaturesUserManagementServiceCollectionExtensions.cs @@ -13,4 +13,4 @@ public static class FeaturesUserManagementServiceCollectionExtensions services.AddScoped(); return services; } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.UserManagement/Features.UserManagement.csproj b/web/backend/Features/Features.UserManagement/Features.UserManagement.csproj index b43da236..0aa52271 100644 --- a/web/backend/Features/Features.UserManagement/Features.UserManagement.csproj +++ b/web/backend/Features/Features.UserManagement/Features.UserManagement.csproj @@ -7,14 +7,14 @@ - + - - - - - + + + + + diff --git a/web/backend/Features/Features.UserManagement/Queries/GetAllUsers/GetAllUsersHandler.cs b/web/backend/Features/Features.UserManagement/Queries/GetAllUsers/GetAllUsersHandler.cs index 88e654b1..10b566af 100644 --- a/web/backend/Features/Features.UserManagement/Queries/GetAllUsers/GetAllUsersHandler.cs +++ b/web/backend/Features/Features.UserManagement/Queries/GetAllUsers/GetAllUsersHandler.cs @@ -11,8 +11,11 @@ namespace Features.UserManagement.Queries.GetAllUsers; public class GetAllUsersHandler(IUserAccountRepository repository) : IRequestHandler> { - public Task> Handle(GetAllUsersQuery request, CancellationToken cancellationToken) + public Task> Handle( + GetAllUsersQuery request, + CancellationToken cancellationToken + ) { return repository.GetAllAsync(request.Limit, request.Offset); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.UserManagement/Queries/GetAllUsers/GetAllUsersQuery.cs b/web/backend/Features/Features.UserManagement/Queries/GetAllUsers/GetAllUsersQuery.cs index abac3a7e..a1a32416 100644 --- a/web/backend/Features/Features.UserManagement/Queries/GetAllUsers/GetAllUsersQuery.cs +++ b/web/backend/Features/Features.UserManagement/Queries/GetAllUsers/GetAllUsersQuery.cs @@ -6,4 +6,4 @@ namespace Features.UserManagement.Queries.GetAllUsers; /// /// Retrieves a paginated list of user accounts. /// -public record GetAllUsersQuery(int? Limit, int? Offset) : IRequest>; \ No newline at end of file +public record GetAllUsersQuery(int? Limit, int? Offset) : IRequest>; diff --git a/web/backend/Features/Features.UserManagement/Queries/GetUserById/GetUserByIdHandler.cs b/web/backend/Features/Features.UserManagement/Queries/GetUserById/GetUserByIdHandler.cs index 5c3e170e..405afec7 100644 --- a/web/backend/Features/Features.UserManagement/Queries/GetUserById/GetUserByIdHandler.cs +++ b/web/backend/Features/Features.UserManagement/Queries/GetUserById/GetUserByIdHandler.cs @@ -13,11 +13,14 @@ public class GetUserByIdHandler(IUserAccountRepository repository) : IRequestHandler { /// Thrown when no user account exists with the given ID. - public async Task Handle(GetUserByIdQuery request, CancellationToken cancellationToken) + public async Task Handle( + GetUserByIdQuery request, + CancellationToken cancellationToken + ) { UserAccount? user = await repository.GetByIdAsync(request.UserAccountId); if (user is null) throw new NotFoundException($"User with ID {request.UserAccountId} not found"); return user; } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.UserManagement/Queries/GetUserById/GetUserByIdQuery.cs b/web/backend/Features/Features.UserManagement/Queries/GetUserById/GetUserByIdQuery.cs index 9f88e6f1..dab2a1cd 100644 --- a/web/backend/Features/Features.UserManagement/Queries/GetUserById/GetUserByIdQuery.cs +++ b/web/backend/Features/Features.UserManagement/Queries/GetUserById/GetUserByIdQuery.cs @@ -6,4 +6,4 @@ namespace Features.UserManagement.Queries.GetUserById; /// /// Retrieves a single user account by its unique identifier. /// -public record GetUserByIdQuery(Guid UserAccountId) : IRequest; \ No newline at end of file +public record GetUserByIdQuery(Guid UserAccountId) : IRequest; diff --git a/web/backend/Features/Features.UserManagement/Repository/IUserAccountRepository.cs b/web/backend/Features/Features.UserManagement/Repository/IUserAccountRepository.cs index 449d9ae1..94973010 100644 --- a/web/backend/Features/Features.UserManagement/Repository/IUserAccountRepository.cs +++ b/web/backend/Features/Features.UserManagement/Repository/IUserAccountRepository.cs @@ -20,10 +20,7 @@ public interface IUserAccountRepository /// The maximum number of records to return, or null for no limit. /// The number of records to skip, or null for no offset. /// The collection of matching records. - Task> GetAllAsync( - int? limit, - int? offset - ); + Task> GetAllAsync(int? limit, int? offset); /// /// Updates an existing user account's details. @@ -50,4 +47,4 @@ public interface IUserAccountRepository /// The email address to search for. /// The matching , or null if not found. Task GetByEmailAsync(string email); -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.UserManagement/Repository/UserAccountRepository.cs b/web/backend/Features/Features.UserManagement/Repository/UserAccountRepository.cs index 210f38ea..743d05e1 100644 --- a/web/backend/Features/Features.UserManagement/Repository/UserAccountRepository.cs +++ b/web/backend/Features/Features.UserManagement/Repository/UserAccountRepository.cs @@ -42,10 +42,7 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory) /// The number of records to skip, or null for no offset. /// The collection of matching records. /// Thrown when the database command fails. - public async Task> GetAllAsync( - int? limit, - int? offset - ) + public async Task> GetAllAsync(int? limit, int? offset) { await using DbConnection connection = await CreateConnection(); await using DbCommand command = connection.CreateCommand(); @@ -61,7 +58,8 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory) await using DbDataReader reader = await command.ExecuteReaderAsync(); List users = new(); - while (await reader.ReadAsync()) users.Add(MapToEntity(reader)); + while (await reader.ReadAsync()) + users.Add(MapToEntity(reader)); return users; } @@ -111,9 +109,7 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory) /// The username to search for. /// The matching , or null if not found. /// Thrown when the database command fails. - public async Task GetByUsernameAsync( - string username - ) + public async Task GetByUsernameAsync(string username) { await using DbConnection connection = await CreateConnection(); await using DbCommand command = connection.CreateCommand(); @@ -132,9 +128,7 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory) /// The email address to search for. /// The matching , or null if not found. /// Thrown when the database command fails. - public async Task GetByEmailAsync( - string email - ) + public async Task GetByEmailAsync(string email) { await using DbConnection connection = await CreateConnection(); await using DbCommand command = connection.CreateCommand(); @@ -152,9 +146,7 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory) /// /// The data reader positioned on the row to map. /// The mapped instance. - protected override UserAccount MapToEntity( - DbDataReader reader - ) + protected override UserAccount MapToEntity(DbDataReader reader) { return new UserAccount { @@ -168,9 +160,7 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory) ? null : reader.GetDateTime(reader.GetOrdinal("UpdatedAt")), DateOfBirth = reader.GetDateTime(reader.GetOrdinal("DateOfBirth")), - Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) - ? null - : (byte[])reader["Timer"] + Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) ? null : (byte[])reader["Timer"], }; } @@ -181,15 +171,11 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory) /// The command to add the parameter to. /// The parameter name (including any prefix, e.g. "@Username"). /// The parameter value, or null to bind . - private static void AddParameter( - DbCommand command, - string name, - object? value - ) + private static void AddParameter(DbCommand command, string name, object? value) { DbParameter p = command.CreateParameter(); p.ParameterName = name; p.Value = value ?? DBNull.Value; command.Parameters.Add(p); } -} \ No newline at end of file +} diff --git a/web/backend/Infrastructure/Infrastructure.Email.Templates/Infrastructure.Email.Templates.csproj b/web/backend/Infrastructure/Infrastructure.Email.Templates/Infrastructure.Email.Templates.csproj index c52e3a84..103e64de 100644 --- a/web/backend/Infrastructure/Infrastructure.Email.Templates/Infrastructure.Email.Templates.csproj +++ b/web/backend/Infrastructure/Infrastructure.Email.Templates/Infrastructure.Email.Templates.csproj @@ -6,21 +6,15 @@ - + + - - + diff --git a/web/backend/Infrastructure/Infrastructure.Email.Templates/Rendering/EmailTemplateProvider.cs b/web/backend/Infrastructure/Infrastructure.Email.Templates/Rendering/EmailTemplateProvider.cs index 14281a3d..3759ccb9 100644 --- a/web/backend/Infrastructure/Infrastructure.Email.Templates/Rendering/EmailTemplateProvider.cs +++ b/web/backend/Infrastructure/Infrastructure.Email.Templates/Rendering/EmailTemplateProvider.cs @@ -14,10 +14,8 @@ namespace Infrastructure.Email.Templates.Rendering; /// pipeline. /// /// The logger factory passed to the used to render components. -public class EmailTemplateProvider( - IServiceProvider serviceProvider, - ILoggerFactory loggerFactory -) : IEmailTemplateProvider +public class EmailTemplateProvider(IServiceProvider serviceProvider, ILoggerFactory loggerFactory) + : IEmailTemplateProvider { /// /// Renders the UserRegisteredEmail template with the specified parameters. @@ -33,7 +31,7 @@ public class EmailTemplateProvider( Dictionary parameters = new() { { nameof(UserRegistration.Username), username }, - { nameof(UserRegistration.ConfirmationLink), confirmationLink } + { nameof(UserRegistration.ConfirmationLink), confirmationLink }, }; return await RenderComponentAsync(parameters); @@ -53,7 +51,7 @@ public class EmailTemplateProvider( Dictionary parameters = new() { { nameof(ResendConfirmation.Username), username }, - { nameof(ResendConfirmation.ConfirmationLink), confirmationLink } + { nameof(ResendConfirmation.ConfirmationLink), confirmationLink }, }; return await RenderComponentAsync(parameters); @@ -72,10 +70,7 @@ public class EmailTemplateProvider( ) where TComponent : IComponent { - await using HtmlRenderer htmlRenderer = new( - serviceProvider, - loggerFactory - ); + await using HtmlRenderer htmlRenderer = new(serviceProvider, loggerFactory); string html = await htmlRenderer.Dispatcher.InvokeAsync(async () => { @@ -89,4 +84,4 @@ public class EmailTemplateProvider( return html; } -} \ No newline at end of file +} diff --git a/web/backend/Infrastructure/Infrastructure.Email.Templates/Rendering/IEmailTemplateProvider.cs b/web/backend/Infrastructure/Infrastructure.Email.Templates/Rendering/IEmailTemplateProvider.cs index 77d3df0f..97785db2 100644 --- a/web/backend/Infrastructure/Infrastructure.Email.Templates/Rendering/IEmailTemplateProvider.cs +++ b/web/backend/Infrastructure/Infrastructure.Email.Templates/Rendering/IEmailTemplateProvider.cs @@ -11,10 +11,7 @@ public interface IEmailTemplateProvider /// The username to include in the email /// The email confirmation link /// The rendered HTML string - Task RenderUserRegisteredEmailAsync( - string username, - string confirmationLink - ); + Task RenderUserRegisteredEmailAsync(string username, string confirmationLink); /// /// Renders the ResendConfirmation template with the specified parameters. @@ -22,8 +19,5 @@ public interface IEmailTemplateProvider /// The username to include in the email /// The new confirmation link /// The rendered HTML string - Task RenderResendConfirmationEmailAsync( - string username, - string confirmationLink - ); -} \ No newline at end of file + Task RenderResendConfirmationEmailAsync(string username, string confirmationLink); +} diff --git a/web/backend/Infrastructure/Infrastructure.Email/IEmailProvider.cs b/web/backend/Infrastructure/Infrastructure.Email/IEmailProvider.cs index 7efd462c..bf466752 100644 --- a/web/backend/Infrastructure/Infrastructure.Email/IEmailProvider.cs +++ b/web/backend/Infrastructure/Infrastructure.Email/IEmailProvider.cs @@ -21,10 +21,5 @@ public interface IEmailProvider /// Email subject line /// Email body (HTML or plain text) /// Whether the body is HTML (default: true) - Task SendAsync( - IEnumerable to, - string subject, - string body, - bool isHtml = true - ); -} \ No newline at end of file + Task SendAsync(IEnumerable to, string subject, string body, bool isHtml = true); +} diff --git a/web/backend/Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj b/web/backend/Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj index f95550c3..1ce8eb9b 100644 --- a/web/backend/Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj +++ b/web/backend/Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj @@ -7,6 +7,6 @@ - + diff --git a/web/backend/Infrastructure/Infrastructure.Email/SmtpEmailProvider.cs b/web/backend/Infrastructure/Infrastructure.Email/SmtpEmailProvider.cs index 706fca85..5b15f1a6 100644 --- a/web/backend/Infrastructure/Infrastructure.Email/SmtpEmailProvider.cs +++ b/web/backend/Infrastructure/Infrastructure.Email/SmtpEmailProvider.cs @@ -58,22 +58,16 @@ public class SmtpEmailProvider : IEmailProvider { _host = Environment.GetEnvironmentVariable("SMTP_HOST") - ?? throw new InvalidOperationException( - "SMTP_HOST environment variable is not set" - ); + ?? throw new InvalidOperationException("SMTP_HOST environment variable is not set"); - string portString = - Environment.GetEnvironmentVariable("SMTP_PORT") ?? "587"; + string portString = Environment.GetEnvironmentVariable("SMTP_PORT") ?? "587"; if (!int.TryParse(portString, out _port)) - throw new InvalidOperationException( - $"SMTP_PORT '{portString}' is not a valid integer" - ); + throw new InvalidOperationException($"SMTP_PORT '{portString}' is not a valid integer"); _username = Environment.GetEnvironmentVariable("SMTP_USERNAME"); _password = Environment.GetEnvironmentVariable("SMTP_PASSWORD"); - string useSslString = - Environment.GetEnvironmentVariable("SMTP_USE_SSL") ?? "true"; + string useSslString = Environment.GetEnvironmentVariable("SMTP_USE_SSL") ?? "true"; _useSsl = bool.Parse(useSslString); _fromEmail = @@ -82,9 +76,7 @@ public class SmtpEmailProvider : IEmailProvider "SMTP_FROM_EMAIL environment variable is not set" ); - _fromName = - Environment.GetEnvironmentVariable("SMTP_FROM_NAME") - ?? "The Biergarten"; + _fromName = Environment.GetEnvironmentVariable("SMTP_FROM_NAME") ?? "The Biergarten"; } /// @@ -95,12 +87,7 @@ public class SmtpEmailProvider : IEmailProvider /// Email body (HTML or plain text) /// Whether the body is HTML (default: true) /// Thrown when connecting, authenticating, or sending via SMTP fails. - public async Task SendAsync( - string to, - string subject, - string body, - bool isHtml = true - ) + public async Task SendAsync(string to, string subject, string body, bool isHtml = true) { await SendAsync([to], subject, body, isHtml); } @@ -128,7 +115,8 @@ public class SmtpEmailProvider : IEmailProvider MimeMessage message = new(); message.From.Add(new MailboxAddress(_fromName, _fromEmail)); - foreach (string recipient in to) message.To.Add(MailboxAddress.Parse(recipient)); + foreach (string recipient in to) + message.To.Add(MailboxAddress.Parse(recipient)); message.Subject = subject; @@ -152,10 +140,7 @@ public class SmtpEmailProvider : IEmailProvider await client.ConnectAsync(_host, _port, secureSocketOptions); // Authenticate if credentials are provided - if ( - !string.IsNullOrEmpty(_username) - && !string.IsNullOrEmpty(_password) - ) + if (!string.IsNullOrEmpty(_username) && !string.IsNullOrEmpty(_password)) await client.AuthenticateAsync(_username, _password); await client.SendAsync(message); @@ -163,10 +148,7 @@ public class SmtpEmailProvider : IEmailProvider } catch (Exception ex) { - throw new InvalidOperationException( - $"Failed to send email: {ex.Message}", - ex - ); + throw new InvalidOperationException($"Failed to send email: {ex.Message}", ex); } } -} \ No newline at end of file +} diff --git a/web/backend/Infrastructure/Infrastructure.Jwt/ITokenInfrastructure.cs b/web/backend/Infrastructure/Infrastructure.Jwt/ITokenInfrastructure.cs index 7fbf2404..c26ea328 100644 --- a/web/backend/Infrastructure/Infrastructure.Jwt/ITokenInfrastructure.cs +++ b/web/backend/Infrastructure/Infrastructure.Jwt/ITokenInfrastructure.cs @@ -15,12 +15,7 @@ public interface ITokenInfrastructure /// The date and time at which the token expires. /// The symmetric secret used to sign the token. /// The serialized, signed JWT string. - string GenerateJwt( - Guid userId, - string username, - DateTime expiry, - string secret - ); + string GenerateJwt(Guid userId, string username, DateTime expiry, string secret); /// /// Validates a JWT and returns the resulting claims principal. @@ -33,4 +28,4 @@ public interface ITokenInfrastructure /// validation. /// Task ValidateJwtAsync(string token, string secret); -} \ No newline at end of file +} diff --git a/web/backend/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj b/web/backend/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj index 3823c8d4..d66c84f5 100644 --- a/web/backend/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj +++ b/web/backend/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj @@ -7,17 +7,11 @@ - - + + - + diff --git a/web/backend/Infrastructure/Infrastructure.Jwt/JwtInfrastructure.cs b/web/backend/Infrastructure/Infrastructure.Jwt/JwtInfrastructure.cs index 165197bb..72837bb9 100644 --- a/web/backend/Infrastructure/Infrastructure.Jwt/JwtInfrastructure.cs +++ b/web/backend/Infrastructure/Infrastructure.Jwt/JwtInfrastructure.cs @@ -25,12 +25,7 @@ public class JwtInfrastructure : ITokenInfrastructure /// The date and time at which the token expires. /// The symmetric secret used to sign the token (encoded as UTF-8 bytes). /// The serialized, signed JWT string. - public string GenerateJwt( - Guid userId, - string username, - DateTime expiry, - string secret - ) + public string GenerateJwt(Guid userId, string username, DateTime expiry, string secret) { JsonWebTokenHandler handler = new(); byte[] key = Encoding.UTF8.GetBytes(secret); @@ -46,7 +41,7 @@ public class JwtInfrastructure : ITokenInfrastructure JwtRegisteredClaimNames.Exp, new DateTimeOffset(expiry).ToUnixTimeSeconds().ToString() ), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; SecurityTokenDescriptor tokenDescriptor = new() @@ -56,13 +51,12 @@ public class JwtInfrastructure : ITokenInfrastructure SigningCredentials = new SigningCredentials( new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256 - ) + ), }; return handler.CreateToken(tokenDescriptor); } - /// /// Validates a JWT's signature and lifetime (issuer and audience validation are disabled), /// using the provided secret as the HMAC-SHA256 symmetric signing key. @@ -74,21 +68,16 @@ public class JwtInfrastructure : ITokenInfrastructure /// Thrown when the token is invalid, has no claims identity, is expired, or otherwise fails validation /// (including when validation itself throws, e.g. due to a malformed token or signature mismatch). /// - public async Task ValidateJwtAsync( - string token, - string secret - ) + public async Task ValidateJwtAsync(string token, string secret) { JsonWebTokenHandler handler = new(); - byte[] keyBytes = Encoding.UTF8.GetBytes( - secret - ); + byte[] keyBytes = Encoding.UTF8.GetBytes(secret); TokenValidationParameters parameters = new() { ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true, - IssuerSigningKey = new SymmetricSecurityKey(keyBytes) + IssuerSigningKey = new SymmetricSecurityKey(keyBytes), }; try @@ -104,4 +93,4 @@ public class JwtInfrastructure : ITokenInfrastructure throw new UnauthorizedException("Invalid token"); } } -} \ No newline at end of file +} diff --git a/web/backend/Infrastructure/Infrastructure.PasswordHashing/Argon2Infrastructure.cs b/web/backend/Infrastructure/Infrastructure.PasswordHashing/Argon2Infrastructure.cs index 9f4ba502..9914745c 100644 --- a/web/backend/Infrastructure/Infrastructure.PasswordHashing/Argon2Infrastructure.cs +++ b/web/backend/Infrastructure/Infrastructure.PasswordHashing/Argon2Infrastructure.cs @@ -32,7 +32,7 @@ public class Argon2Infrastructure : IPasswordInfrastructure Salt = salt, DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1), MemorySize = ArgonMemoryKb, - Iterations = ArgonIterations + Iterations = ArgonIterations, }; byte[] hash = argon2.GetBytes(HashSize); @@ -57,10 +57,7 @@ public class Argon2Infrastructure : IPasswordInfrastructure { try { - string[] parts = stored.Split( - ':', - StringSplitOptions.RemoveEmptyEntries - ); + string[] parts = stored.Split(':', StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2) return false; @@ -72,7 +69,7 @@ public class Argon2Infrastructure : IPasswordInfrastructure Salt = salt, DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1), MemorySize = ArgonMemoryKb, - Iterations = ArgonIterations + Iterations = ArgonIterations, }; byte[] actual = argon2.GetBytes(expected.Length); @@ -83,4 +80,4 @@ public class Argon2Infrastructure : IPasswordInfrastructure return false; } } -} \ No newline at end of file +} diff --git a/web/backend/Infrastructure/Infrastructure.PasswordHashing/IPasswordInfrastructure.cs b/web/backend/Infrastructure/Infrastructure.PasswordHashing/IPasswordInfrastructure.cs index 3b04e21a..bb771238 100644 --- a/web/backend/Infrastructure/Infrastructure.PasswordHashing/IPasswordInfrastructure.cs +++ b/web/backend/Infrastructure/Infrastructure.PasswordHashing/IPasswordInfrastructure.cs @@ -19,4 +19,4 @@ public interface IPasswordInfrastructure /// The stored salt/hash string previously produced by . /// true if the password matches the stored hash; otherwise false. public bool Verify(string password, string stored); -} \ No newline at end of file +} diff --git a/web/backend/Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj b/web/backend/Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj index 44381592..614d5e40 100644 --- a/web/backend/Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj +++ b/web/backend/Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj @@ -7,9 +7,6 @@ - + diff --git a/web/backend/Infrastructure/Infrastructure.Sql/DefaultSqlConnectionFactory.cs b/web/backend/Infrastructure/Infrastructure.Sql/DefaultSqlConnectionFactory.cs index fff740ec..5ef7cc9d 100644 --- a/web/backend/Infrastructure/Infrastructure.Sql/DefaultSqlConnectionFactory.cs +++ b/web/backend/Infrastructure/Infrastructure.Sql/DefaultSqlConnectionFactory.cs @@ -9,12 +9,9 @@ namespace Infrastructure.Sql; /// resolving the connection string from environment variables or application configuration. /// /// The application configuration, used as a fallback source for the connection string. -public class DefaultSqlConnectionFactory(IConfiguration configuration) - : ISqlConnectionFactory +public class DefaultSqlConnectionFactory(IConfiguration configuration) : ISqlConnectionFactory { - private readonly string _connectionString = GetConnectionString( - configuration - ); + private readonly string _connectionString = GetConnectionString(configuration); /// /// Creates a new, unopened using the resolved connection string. @@ -39,10 +36,9 @@ public class DefaultSqlConnectionFactory(IConfiguration configuration) private static string GetConnectionString(IConfiguration configuration) { // Check for full connection string first - string? fullConnectionString = Environment.GetEnvironmentVariable( - "DB_CONNECTION_STRING" - ); - if (!string.IsNullOrEmpty(fullConnectionString)) return fullConnectionString; + string? fullConnectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING"); + if (!string.IsNullOrEmpty(fullConnectionString)) + return fullConnectionString; // Try to build from individual environment variables (preferred method for Docker) try @@ -53,11 +49,12 @@ public class DefaultSqlConnectionFactory(IConfiguration configuration) { // Fall back to configuration-based connection string if env vars are not set string? connString = configuration.GetConnectionString("Default"); - if (!string.IsNullOrEmpty(connString)) return connString; + if (!string.IsNullOrEmpty(connString)) + return connString; throw new InvalidOperationException( "Database connection string not configured. Set DB_CONNECTION_STRING or DB_SERVER, DB_NAME, DB_USER, DB_PASSWORD env vars or ConnectionStrings:Default." ); } } -} \ No newline at end of file +} diff --git a/web/backend/Infrastructure/Infrastructure.Sql/ISqlConnectionFactory.cs b/web/backend/Infrastructure/Infrastructure.Sql/ISqlConnectionFactory.cs index 78c26d68..89976560 100644 --- a/web/backend/Infrastructure/Infrastructure.Sql/ISqlConnectionFactory.cs +++ b/web/backend/Infrastructure/Infrastructure.Sql/ISqlConnectionFactory.cs @@ -12,4 +12,4 @@ public interface ISqlConnectionFactory /// /// A new instance. DbConnection CreateConnection(); -} \ No newline at end of file +} diff --git a/web/backend/Infrastructure/Infrastructure.Sql/Infrastructure.Sql.csproj b/web/backend/Infrastructure/Infrastructure.Sql/Infrastructure.Sql.csproj index b9ea4fa4..7ff048e9 100644 --- a/web/backend/Infrastructure/Infrastructure.Sql/Infrastructure.Sql.csproj +++ b/web/backend/Infrastructure/Infrastructure.Sql/Infrastructure.Sql.csproj @@ -6,15 +6,12 @@ Infrastructure.Sql - + + + - - diff --git a/web/backend/Infrastructure/Infrastructure.Sql/Repository.cs b/web/backend/Infrastructure/Infrastructure.Sql/Repository.cs index 6691682b..b223cd12 100644 --- a/web/backend/Infrastructure/Infrastructure.Sql/Repository.cs +++ b/web/backend/Infrastructure/Infrastructure.Sql/Repository.cs @@ -29,4 +29,4 @@ public abstract class Repository(ISqlConnectionFactory connectionFactory) /// The data reader positioned on the row to map. /// The mapped entity instance. protected abstract T MapToEntity(DbDataReader reader); -} \ No newline at end of file +} diff --git a/web/backend/Infrastructure/Infrastructure.Sql/SqlConnectionStringHelper.cs b/web/backend/Infrastructure/Infrastructure.Sql/SqlConnectionStringHelper.cs index 4ccdc1f0..a5ecb395 100644 --- a/web/backend/Infrastructure/Infrastructure.Sql/SqlConnectionStringHelper.cs +++ b/web/backend/Infrastructure/Infrastructure.Sql/SqlConnectionStringHelper.cs @@ -17,28 +17,20 @@ public static class SqlConnectionStringHelper { string server = Environment.GetEnvironmentVariable("DB_SERVER") - ?? throw new InvalidOperationException( - "DB_SERVER environment variable is not set" - ); + ?? throw new InvalidOperationException("DB_SERVER environment variable is not set"); string dbName = databaseName ?? Environment.GetEnvironmentVariable("DB_NAME") - ?? throw new InvalidOperationException( - "DB_NAME environment variable is not set" - ); + ?? throw new InvalidOperationException("DB_NAME environment variable is not set"); string user = Environment.GetEnvironmentVariable("DB_USER") - ?? throw new InvalidOperationException( - "DB_USER environment variable is not set" - ); + ?? throw new InvalidOperationException("DB_USER environment variable is not set"); string password = Environment.GetEnvironmentVariable("DB_PASSWORD") - ?? throw new InvalidOperationException( - "DB_PASSWORD environment variable is not set" - ); + ?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set"); SqlConnectionStringBuilder builder = new() { @@ -47,7 +39,7 @@ public static class SqlConnectionStringHelper UserID = user, Password = password, TrustServerCertificate = true, - Encrypt = true + Encrypt = true, }; return builder.ConnectionString; @@ -61,4 +53,4 @@ public static class SqlConnectionStringHelper { return BuildConnectionString("master"); } -} \ No newline at end of file +} diff --git a/web/backend/Shared/Shared.Application/Behaviors/ValidationBehavior.cs b/web/backend/Shared/Shared.Application/Behaviors/ValidationBehavior.cs index 8720f9eb..e41d152b 100644 --- a/web/backend/Shared/Shared.Application/Behaviors/ValidationBehavior.cs +++ b/web/backend/Shared/Shared.Application/Behaviors/ValidationBehavior.cs @@ -19,21 +19,21 @@ public class ValidationBehavior(IEnumerable context = new(request); List failures = ( - await Task.WhenAll( - validators.Select(v => v.ValidateAsync(context, cancellationToken)) - ) - ) + await Task.WhenAll(validators.Select(v => v.ValidateAsync(context, cancellationToken))) + ) .SelectMany(result => result.Errors) .Where(failure => failure != null) .ToList(); - if (failures.Count > 0) throw new ValidationException(failures); + if (failures.Count > 0) + throw new ValidationException(failures); return await next(); } -} \ No newline at end of file +} diff --git a/web/backend/Shared/Shared.Application/Emails/SendRegistrationEmailCommand.cs b/web/backend/Shared/Shared.Application/Emails/SendRegistrationEmailCommand.cs index 2d297265..5c573245 100644 --- a/web/backend/Shared/Shared.Application/Emails/SendRegistrationEmailCommand.cs +++ b/web/backend/Shared/Shared.Application/Emails/SendRegistrationEmailCommand.cs @@ -6,8 +6,5 @@ namespace Shared.Application.Emails; /// Cross-slice command sent by Features.Auth and handled by Features.Emails to dispatch a /// registration confirmation email, without either slice taking a project reference on the other. /// -public record SendRegistrationEmailCommand( - string FirstName, - string Email, - string ConfirmationToken -) : IRequest; \ No newline at end of file +public record SendRegistrationEmailCommand(string FirstName, string Email, string ConfirmationToken) + : IRequest; diff --git a/web/backend/Shared/Shared.Application/Emails/SendResendConfirmationEmailCommand.cs b/web/backend/Shared/Shared.Application/Emails/SendResendConfirmationEmailCommand.cs index 7783f3df..0831d8c2 100644 --- a/web/backend/Shared/Shared.Application/Emails/SendResendConfirmationEmailCommand.cs +++ b/web/backend/Shared/Shared.Application/Emails/SendResendConfirmationEmailCommand.cs @@ -10,4 +10,4 @@ public record SendResendConfirmationEmailCommand( string FirstName, string Email, string ConfirmationToken -) : IRequest; \ No newline at end of file +) : IRequest; diff --git a/web/backend/Shared/Shared.Application/Shared.Application.csproj b/web/backend/Shared/Shared.Application/Shared.Application.csproj index 519d781a..be50cf0f 100644 --- a/web/backend/Shared/Shared.Application/Shared.Application.csproj +++ b/web/backend/Shared/Shared.Application/Shared.Application.csproj @@ -7,7 +7,7 @@ - - + + diff --git a/web/backend/Shared/Shared.Contracts/ResponseBody.cs b/web/backend/Shared/Shared.Contracts/ResponseBody.cs index c8522b27..4fc35c4f 100644 --- a/web/backend/Shared/Shared.Contracts/ResponseBody.cs +++ b/web/backend/Shared/Shared.Contracts/ResponseBody.cs @@ -26,4 +26,4 @@ public record ResponseBody /// A human-readable message describing the outcome of the request. /// public required string Message { get; init; } -} \ No newline at end of file +}