code style cleanup

This commit is contained in:
Aaron Po
2026-06-20 13:55:17 -04:00
parent 0c3b0e99e8
commit 5b882ac51c
167 changed files with 3711 additions and 3522 deletions

View File

@@ -9,32 +9,32 @@
<ItemGroup>
<PackageReference
Include="Microsoft.AspNetCore.OpenApi"
Version="9.0.11"
Include="Microsoft.AspNetCore.OpenApi"
Version="9.0.11"
/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
<PackageReference
Include="FluentValidation.AspNetCore"
Version="11.3.0"
Include="FluentValidation.AspNetCore"
Version="11.3.0"
/>
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="MediatR" Version="12.4.1"/>
</ItemGroup>
<ItemGroup>
<Folder Include="Infrastructure\" />
<Folder Include="Infrastructure\"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Sql\Infrastructure.Sql.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
<ProjectReference Include="..\..\Features\Features.Breweries\Features.Breweries.csproj" />
<ProjectReference Include="..\..\Features\Features.UserManagement\Features.UserManagement.csproj" />
<ProjectReference Include="..\..\Features\Features.Auth\Features.Auth.csproj" />
<ProjectReference Include="..\..\Features\Features.Emails\Features.Emails.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj"/>
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj"/>
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Sql\Infrastructure.Sql.csproj"/>
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj"/>
<ProjectReference Include="..\..\Features\Features.Breweries\Features.Breweries.csproj"/>
<ProjectReference Include="..\..\Features\Features.UserManagement\Features.UserManagement.csproj"/>
<ProjectReference Include="..\..\Features\Features.Auth\Features.Auth.csproj"/>
<ProjectReference Include="..\..\Features\Features.Emails\Features.Emails.csproj"/>
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj"/>
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj"/>
</ItemGroup>
<ItemGroup>

View File

@@ -1,20 +1,20 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using Shared.Contracts;
using Infrastructure.Jwt;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Shared.Contracts;
namespace API.Core.Authentication;
/// <summary>
/// Custom ASP.NET Core authentication handler that validates bearer JWT access tokens
/// supplied in the <c>Authorization</c> header against the <c>JWT</c> authentication scheme.
/// Custom ASP.NET Core authentication handler that validates bearer JWT access tokens
/// supplied in the <c>Authorization</c> header against the <c>JWT</c> authentication scheme.
/// </summary>
/// <param name="options">The monitored options for the <c>JWT</c> authentication scheme.</param>
/// <param name="logger">The logger factory used by the base <see cref="AuthenticationHandler{TOptions}"/>.</param>
/// <param name="encoder">The URL encoder used by the base <see cref="AuthenticationHandler{TOptions}"/>.</param>
/// <param name="logger">The logger factory used by the base <see cref="AuthenticationHandler{TOptions}" />.</param>
/// <param name="encoder">The URL encoder used by the base <see cref="AuthenticationHandler{TOptions}" />.</param>
/// <param name="tokenInfrastructure">The infrastructure service used to validate JWT access tokens.</param>
/// <param name="configuration">The application configuration, used as a fallback source for the JWT secret key.</param>
public class JwtAuthenticationHandler(
@@ -26,66 +26,57 @@ public class JwtAuthenticationHandler(
) : AuthenticationHandler<JwtAuthenticationOptions>(options, logger, encoder)
{
/// <summary>
/// Validates the incoming request's bearer JWT access token and produces an authentication result.
/// Validates the incoming request's bearer JWT access token and produces an authentication result.
/// </summary>
/// <remarks>
/// The signing secret is resolved first from the <c>ACCESS_TOKEN_SECRET</c> environment variable, falling
/// back to the <c>Jwt:SecretKey</c> configuration value, to stay consistent with the secret source used
/// when tokens are issued. Authentication fails if the secret is not configured, the
/// <c>Authorization</c> header is missing, the header does not use the <c>Bearer</c> scheme, or the
/// token fails validation.
/// The signing secret is resolved first from the <c>ACCESS_TOKEN_SECRET</c> environment variable, falling
/// back to the <c>Jwt:SecretKey</c> configuration value, to stay consistent with the secret source used
/// when tokens are issued. Authentication fails if the secret is not configured, the
/// <c>Authorization</c> header is missing, the header does not use the <c>Bearer</c> scheme, or the
/// token fails validation.
/// </remarks>
/// <returns>
/// A successful <see cref="AuthenticateResult"/> containing the validated <see cref="System.Security.Claims.ClaimsPrincipal"/>
/// on success, or a failure result describing why authentication could not be completed.
/// A successful <see cref="AuthenticateResult" /> containing the validated
/// <see cref="System.Security.Claims.ClaimsPrincipal" />
/// on success, or a failure result describing why authentication could not be completed.
/// </returns>
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// Use the same access-token secret source as TokenService to avoid mismatched validation.
var secret = Environment.GetEnvironmentVariable("ACCESS_TOKEN_SECRET");
if (string.IsNullOrWhiteSpace(secret))
{
secret = configuration["Jwt:SecretKey"];
}
string? secret = Environment.GetEnvironmentVariable("ACCESS_TOKEN_SECRET");
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 var authHeaderValue
out StringValues authHeaderValue
)
)
{
return AuthenticateResult.Fail("Authorization header is missing");
}
var authHeader = authHeaderValue.ToString();
string authHeader = authHeaderValue.ToString();
if (
!authHeader.StartsWith(
"Bearer ",
StringComparison.OrdinalIgnoreCase
)
)
{
return AuthenticateResult.Fail(
"Invalid authorization header format"
);
}
var token = authHeader.Substring("Bearer ".Length).Trim();
string token = authHeader.Substring("Bearer ".Length).Trim();
try
{
var claimsPrincipal = await tokenInfrastructure.ValidateJwtAsync(
ClaimsPrincipal claimsPrincipal = await tokenInfrastructure.ValidateJwtAsync(
token,
secret
);
var ticket = new AuthenticationTicket(claimsPrincipal, Scheme.Name);
AuthenticationTicket ticket = new(claimsPrincipal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
catch (Exception ex)
@@ -97,7 +88,7 @@ public class JwtAuthenticationHandler(
}
/// <summary>
/// Writes a JSON 401 Unauthorized response when authentication fails or is required but not supplied.
/// Writes a JSON 401 Unauthorized response when authentication fails or is required but not supplied.
/// </summary>
/// <param name="properties">The authentication properties associated with the challenge.</param>
/// <returns>A task that completes once the response body has been written.</returns>
@@ -106,13 +97,15 @@ public class JwtAuthenticationHandler(
Response.ContentType = "application/json";
Response.StatusCode = 401;
var response = new ResponseBody { Message = "Unauthorized: Invalid or missing authentication token" };
ResponseBody response = new() { Message = "Unauthorized: Invalid or missing authentication token" };
await Response.WriteAsJsonAsync(response);
}
}
/// <summary>
/// Options for the <c>JWT</c> authentication scheme handled by <see cref="JwtAuthenticationHandler"/>.
/// Options for the <c>JWT</c> authentication scheme handled by <see cref="JwtAuthenticationHandler" />.
/// </summary>
/// <remarks>No additional options are defined beyond those provided by <see cref="AuthenticationSchemeOptions"/>.</remarks>
public class JwtAuthenticationOptions : AuthenticationSchemeOptions { }
/// <remarks>No additional options are defined beyond those provided by <see cref="AuthenticationSchemeOptions" />.</remarks>
public class JwtAuthenticationOptions : AuthenticationSchemeOptions
{
}

View File

@@ -1,27 +1,27 @@
using Microsoft.AspNetCore.Mvc;
namespace API.Core.Controllers
namespace API.Core.Controllers;
/// <summary>
/// Handles requests that do not match any other route, used as the application's fallback controller.
/// </summary>
/// <remarks>
/// Excluded from API explorer/Swagger output via <c>[ApiExplorerSettings(IgnoreApi = true)]</c>.
/// Wired up as the fallback target via <c>app.MapFallbackToController("Handle404", "NotFound")</c> in
/// <c>Program.cs</c>.
/// </remarks>
[ApiController]
[ApiExplorerSettings(IgnoreApi = true)]
[Route("error")] // required
public class NotFoundController : ControllerBase
{
/// <summary>
/// Handles requests that do not match any other route, used as the application's fallback controller.
/// Returns a generic 404 response for any request that did not match a defined route.
/// </summary>
/// <remarks>
/// Excluded from API explorer/Swagger output via <c>[ApiExplorerSettings(IgnoreApi = true)]</c>.
/// Wired up as the fallback target via <c>app.MapFallbackToController("Handle404", "NotFound")</c> in <c>Program.cs</c>.
/// </remarks>
[ApiController]
[ApiExplorerSettings(IgnoreApi = true)]
[Route("error")] // required
public class NotFoundController : ControllerBase
/// <returns>A <c>404 Not Found</c> result with a JSON body containing a "Route not found." message.</returns>
[HttpGet("404")] //required
public IActionResult Handle404()
{
/// <summary>
/// Returns a generic 404 response for any request that did not match a defined route.
/// </summary>
/// <returns>A <c>404 Not Found</c> result with a JSON body containing a "Route not found." message.</returns>
[HttpGet("404")] //required
public IActionResult Handle404()
{
return NotFound(new { message = "Route not found." });
}
return NotFound(new { message = "Route not found." });
}
}
}

View File

@@ -1,12 +1,12 @@
using System.Security.Claims;
using Shared.Contracts;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Shared.Contracts;
namespace API.Core.Controllers;
/// <summary>
/// Sample controller demonstrating an endpoint that requires JWT authentication.
/// Sample controller demonstrating an endpoint that requires JWT authentication.
/// </summary>
[ApiController]
[Route("api/[controller]")]
@@ -14,25 +14,25 @@ namespace API.Core.Controllers;
public class ProtectedController : ControllerBase
{
/// <summary>
/// Returns the authenticated caller's user ID and username, extracted from the JWT claims.
/// Returns the authenticated caller's user ID and username, extracted from the JWT claims.
/// </summary>
/// <returns>
/// A <c>200 OK</c> result wrapping a <see cref="ResponseBody{T}"/> whose payload contains the
/// caller's <c>userId</c> (from <see cref="ClaimTypes.NameIdentifier"/>) and <c>username</c>
/// (from <see cref="ClaimTypes.Name"/>), either of which may be <c>null</c> if not present in the token.
/// A <c>200 OK</c> result wrapping a <see cref="ResponseBody{T}" /> whose payload contains the
/// caller's <c>userId</c> (from <see cref="ClaimTypes.NameIdentifier" />) and <c>username</c>
/// (from <see cref="ClaimTypes.Name" />), either of which may be <c>null</c> if not present in the token.
/// </returns>
[HttpGet]
public ActionResult<ResponseBody<object>> Get()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var username = User.FindFirst(ClaimTypes.Name)?.Value;
string? userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
string? username = User.FindFirst(ClaimTypes.Name)?.Value;
return Ok(
new ResponseBody<object>
{
Message = "Protected endpoint accessed successfully",
Payload = new { userId, username },
Payload = new { userId, username }
}
);
}
}
}

View File

@@ -1,48 +1,88 @@
// API.Core/Filters/GlobalExceptionFilter.cs
using Shared.Contracts;
using Domain.Exceptions;
using FluentValidation;
using Microsoft.Data.SqlClient;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Data.SqlClient;
using Shared.Contracts;
using ValidationException = FluentValidation.ValidationException;
namespace API.Core;
/// <summary>
/// MVC exception filter that converts unhandled exceptions raised by controller actions into
/// consistent JSON error responses with appropriate HTTP status codes.
/// MVC exception filter that converts unhandled exceptions raised by controller actions into
/// consistent JSON error responses with appropriate HTTP status codes.
/// </summary>
/// <param name="logger">Logger used to record unhandled exceptions before they are translated into a response.</param>
public class GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
: IExceptionFilter
{
/// <summary>
/// Logs the exception and sets <see cref="ExceptionContext.Result"/> to an appropriate error response,
/// marking the exception as handled.
/// Logs the exception and sets <see cref="ExceptionContext.Result" /> to an appropriate error response,
/// marking the exception as handled.
/// </summary>
/// <remarks>
/// Maps exception types to responses as follows:
/// <list type="bullet">
/// <item><description><see cref="FluentValidation.ValidationException"/> → <c>400 Bad Request</c> with per-property validation error messages.</description></item>
/// <item><description><see cref="ConflictException"/> → <c>409 Conflict</c> with a <see cref="ResponseBody"/> message.</description></item>
/// <item><description><see cref="NotFoundException"/> → <c>404 Not Found</c> with a <see cref="ResponseBody"/> message.</description></item>
/// <item><description><see cref="UnauthorizedException"/> → <c>401 Unauthorized</c> with a <see cref="ResponseBody"/> message.</description></item>
/// <item><description><see cref="ForbiddenException"/> → <c>403 Forbidden</c> with a <see cref="ResponseBody"/> message.</description></item>
/// <item><description><see cref="SqlException"/> → <c>503 Service Unavailable</c> with a generic database error message.</description></item>
/// <item><description><see cref="Domain.Exceptions.ValidationException"/> → <c>400 Bad Request</c> with a <see cref="ResponseBody"/> message.</description></item>
/// <item><description>Any other exception → <c>500 Internal Server Error</c> with a generic error message.</description></item>
/// </list>
/// Maps exception types to responses as follows:
/// <list type="bullet">
/// <item>
/// <description>
/// <see cref="FluentValidation.ValidationException" /> → <c>400 Bad Request</c> with per-property
/// validation error messages.
/// </description>
/// </item>
/// <item>
/// <description>
/// <see cref="ConflictException" /> → <c>409 Conflict</c> with a <see cref="ResponseBody" />
/// message.
/// </description>
/// </item>
/// <item>
/// <description>
/// <see cref="NotFoundException" /> → <c>404 Not Found</c> with a <see cref="ResponseBody" />
/// message.
/// </description>
/// </item>
/// <item>
/// <description>
/// <see cref="UnauthorizedException" /> → <c>401 Unauthorized</c> with a
/// <see cref="ResponseBody" /> message.
/// </description>
/// </item>
/// <item>
/// <description>
/// <see cref="ForbiddenException" /> → <c>403 Forbidden</c> with a <see cref="ResponseBody" />
/// message.
/// </description>
/// </item>
/// <item>
/// <description>
/// <see cref="SqlException" /> → <c>503 Service Unavailable</c> with a generic database error
/// message.
/// </description>
/// </item>
/// <item>
/// <description>
/// <see cref="Domain.Exceptions.ValidationException" /> → <c>400 Bad Request</c> with a
/// <see cref="ResponseBody" /> message.
/// </description>
/// </item>
/// <item>
/// <description>Any other exception → <c>500 Internal Server Error</c> with a generic error message.</description>
/// </item>
/// </list>
/// </remarks>
/// <param name="context">The context for the unhandled exception, including the exception itself and the result to populate.</param>
/// <param name="context">
/// The context for the unhandled exception, including the exception itself and the result to
/// populate.
/// </param>
public void OnException(ExceptionContext context)
{
logger.LogError(context.Exception, "Unhandled exception occurred");
switch (context.Exception)
{
case FluentValidation.ValidationException fluentValidationException:
var errors = fluentValidationException
case ValidationException fluentValidationException:
Dictionary<string, string[]> errors = fluentValidationException
.Errors.GroupBy(e => e.PropertyName)
.ToDictionary(
g => g.Key,
@@ -60,7 +100,7 @@ public class GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
new ResponseBody { Message = ex.Message }
)
{
StatusCode = 409,
StatusCode = 409
};
context.ExceptionHandled = true;
break;
@@ -70,7 +110,7 @@ public class GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
new ResponseBody { Message = ex.Message }
)
{
StatusCode = 404,
StatusCode = 404
};
context.ExceptionHandled = true;
break;
@@ -80,7 +120,7 @@ public class GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
new ResponseBody { Message = ex.Message }
)
{
StatusCode = 401,
StatusCode = 401
};
context.ExceptionHandled = true;
break;
@@ -90,7 +130,7 @@ public class GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
new ResponseBody { Message = ex.Message }
)
{
StatusCode = 403,
StatusCode = 403
};
context.ExceptionHandled = true;
break;
@@ -100,7 +140,7 @@ public class GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
new ResponseBody { Message = "A database error occurred." }
)
{
StatusCode = 503,
StatusCode = 503
};
context.ExceptionHandled = true;
break;
@@ -110,7 +150,7 @@ public class GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
new ResponseBody { Message = ex.Message }
)
{
StatusCode = 400,
StatusCode = 400
};
context.ExceptionHandled = true;
break;
@@ -119,14 +159,14 @@ public class GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
context.Result = new ObjectResult(
new ResponseBody
{
Message = "An unexpected error occurred",
Message = "An unexpected error occurred"
}
)
{
StatusCode = 500,
StatusCode = 500
};
context.ExceptionHandled = true;
break;
}
}
}
}

View File

@@ -14,13 +14,10 @@ using Infrastructure.Jwt;
using Infrastructure.Sql;
using Shared.Application.Behaviors;
var builder = WebApplication.CreateBuilder(args);
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Global Exception Filter
builder.Services.AddControllers(options =>
{
options.Filters.Add<GlobalExceptionFilter>();
})
builder.Services.AddControllers(options => { options.Filters.Add<GlobalExceptionFilter>(); })
.AddApplicationPart(typeof(BreweryController).Assembly)
.AddApplicationPart(typeof(UserController).Assembly)
.AddApplicationPart(typeof(AuthController).Assembly);
@@ -54,10 +51,7 @@ 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 -------------------------------------------------------------------------------------
@@ -88,7 +82,7 @@ builder
builder.Services.AddAuthorization();
var app = builder.Build();
WebApplication app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
@@ -106,10 +100,10 @@ app.MapControllers();
app.MapFallbackToController("Handle404", "NotFound");
// Graceful shutdown handling
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
IHostApplicationLifetime lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
lifetime.ApplicationStopping.Register(() =>
{
app.Logger.LogInformation("Application is shutting down gracefully...");
});
app.Run();
app.Run();

View File

@@ -8,40 +8,40 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="FluentAssertions" Version="6.9.0" />
<PackageReference Include="dbup" Version="5.0.41" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
<PackageReference Include="xunit" Version="2.9.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2"/>
<PackageReference Include="FluentAssertions" Version="6.9.0"/>
<PackageReference Include="dbup" Version="5.0.41"/>
<!-- Reqnroll core, xUnit adapter and code-behind generator -->
<PackageReference Include="Reqnroll" Version="3.3.3" />
<PackageReference Include="Reqnroll.xUnit" Version="3.3.3" />
<PackageReference Include="Reqnroll" Version="3.3.3"/>
<PackageReference Include="Reqnroll.xUnit" Version="3.3.3"/>
<PackageReference
Include="Reqnroll.Tools.MsBuild.Generation"
Version="3.3.3"
PrivateAssets="all"
Include="Reqnroll.Tools.MsBuild.Generation"
Version="3.3.3"
PrivateAssets="all"
/>
<!-- ASP.NET Core integration testing -->
<PackageReference
Include="Microsoft.AspNetCore.Mvc.Testing"
Version="9.0.1"
Include="Microsoft.AspNetCore.Mvc.Testing"
Version="9.0.1"
/>
</ItemGroup>
<ItemGroup>
<!-- Ensure feature files are included in the project -->
<None Include="Features\**\*.feature" />
<None Include="Features\**\*.feature"/>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
<Using Include="Xunit"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\API.Core\API.Core.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
<ProjectReference Include="..\..\Features\Features.Emails\Features.Emails.csproj" />
<ProjectReference Include="..\API.Core\API.Core.csproj"/>
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj"/>
<ProjectReference Include="..\..\Features\Features.Emails\Features.Emails.csproj"/>
</ItemGroup>
</Project>

View File

@@ -1,51 +1,51 @@
Feature: Protected Endpoint Access Token Validation
As a backend developer
I want protected endpoints to validate access tokens
So that unauthorized requests are rejected
As a backend developer
I want protected endpoints to validate access tokens
So that unauthorized requests are rejected
Scenario: Protected endpoint accepts valid access token
Given the API is running
And I have an existing account
And I am logged in
When I submit a request to a protected endpoint with a valid access token
Then the response has HTTP status 200
Scenario: Protected endpoint accepts valid access token
Given the API is running
And I have an existing account
And I am logged in
When I submit a request to a protected endpoint with a valid access token
Then the response has HTTP status 200
Scenario: Protected endpoint rejects missing access token
Given the API is running
When I submit a request to a protected endpoint without an access token
Then the response has HTTP status 401
Scenario: Protected endpoint rejects missing access token
Given the API is running
When I submit a request to a protected endpoint without an access token
Then the response has HTTP status 401
Scenario: Protected endpoint rejects invalid access token
Given the API is running
When I submit a request to a protected endpoint with an invalid access token
Then the response has HTTP status 401
And the response JSON should have "message" containing "Unauthorized"
Scenario: Protected endpoint rejects invalid access token
Given the API is running
When I submit a request to a protected endpoint with an invalid access token
Then the response has HTTP status 401
And the response JSON should have "message" containing "Unauthorized"
Scenario: Protected endpoint rejects expired access token
Given the API is running
And I have an existing account
And I am logged in with an immediately-expiring access token
When I submit a request to a protected endpoint with the expired token
Then the response has HTTP status 401
And the response JSON should have "message" containing "Unauthorized"
Scenario: Protected endpoint rejects expired access token
Given the API is running
And I have an existing account
And I am logged in with an immediately-expiring access token
When I submit a request to a protected endpoint with the expired token
Then the response has HTTP status 401
And the response JSON should have "message" containing "Unauthorized"
Scenario: Protected endpoint rejects token signed with wrong secret
Given the API is running
And I have an access token signed with the wrong secret
When I submit a request to a protected endpoint with the tampered token
Then the response has HTTP status 401
And the response JSON should have "message" containing "Unauthorized"
Scenario: Protected endpoint rejects token signed with wrong secret
Given the API is running
And I have an access token signed with the wrong secret
When I submit a request to a protected endpoint with the tampered token
Then the response has HTTP status 401
And the response JSON should have "message" containing "Unauthorized"
Scenario: Protected endpoint rejects refresh token as access token
Given the API is running
And I have an existing account
And I am logged in
When I submit a request to a protected endpoint with my refresh token instead of access token
Then the response has HTTP status 401
Scenario: Protected endpoint rejects refresh token as access token
Given the API is running
And I have an existing account
And I am logged in
When I submit a request to a protected endpoint with my refresh token instead of access token
Then the response has HTTP status 401
Scenario: Protected endpoint rejects confirmation token as access token
Given the API is running
And I have registered a new account
And I have a valid confirmation token
When I submit a request to a protected endpoint with my confirmation token instead of access token
Then the response has HTTP status 401
Scenario: Protected endpoint rejects confirmation token as access token
Given the API is running
And I have registered a new account
And I have a valid confirmation token
When I submit a request to a protected endpoint with my confirmation token instead of access token
Then the response has HTTP status 401

View File

@@ -1,76 +1,77 @@
Feature: User Account Confirmation
As a newly registered user
I want to confirm my email address via a validation token
So that my account is fully activated
Scenario: Successful confirmation with valid token
Given the API is running
And I have registered a new account
And I have a valid confirmation token for my account
And I have a valid access token for my account
When I submit a confirmation request with the valid token
Then the response has HTTP status 200
And the response JSON should have "message" containing "is confirmed"
As a newly registered user
I want to confirm my email address via a validation token
So that my account is fully activated
Scenario: Re-confirming an already verified account remains successful
Given the API is running
And I have registered a new account
And I have a valid confirmation token for my account
And I have a valid access token for my account
When I submit a confirmation request with the valid token
And I submit the same confirmation request again
Then the response has HTTP status 200
And the response JSON should have "message" containing "is confirmed"
Scenario: Successful confirmation with valid token
Given the API is running
And I have registered a new account
And I have a valid confirmation token for my account
And I have a valid access token for my account
When I submit a confirmation request with the valid token
Then the response has HTTP status 200
And the response JSON should have "message" containing "is confirmed"
Scenario: Confirmation fails with invalid token
Given the API is running
And I have registered a new account
And I have a valid access token for my account
When I submit a confirmation request with an invalid token
Then the response has HTTP status 401
And the response JSON should have "message" containing "Invalid token"
Scenario: Re-confirming an already verified account remains successful
Given the API is running
And I have registered a new account
And I have a valid confirmation token for my account
And I have a valid access token for my account
When I submit a confirmation request with the valid token
And I submit the same confirmation request again
Then the response has HTTP status 200
And the response JSON should have "message" containing "is confirmed"
Scenario: Confirmation fails with expired token
Given the API is running
And I have registered a new account
And I have an expired confirmation token for my account
And I have a valid access token for my account
When I submit a confirmation request with the expired token
Then the response has HTTP status 401
And the response JSON should have "message" containing "Invalid token"
Scenario: Confirmation fails with invalid token
Given the API is running
And I have registered a new account
And I have a valid access token for my account
When I submit a confirmation request with an invalid token
Then the response has HTTP status 401
And the response JSON should have "message" containing "Invalid token"
Scenario: Confirmation fails with tampered token (wrong secret)
Given the API is running
And I have registered a new account
And I have a confirmation token signed with the wrong secret
And I have a valid access token for my account
When I submit a confirmation request with the tampered token
Then the response has HTTP status 401
And the response JSON should have "message" containing "Invalid token"
Scenario: Confirmation fails with expired token
Given the API is running
And I have registered a new account
And I have an expired confirmation token for my account
And I have a valid access token for my account
When I submit a confirmation request with the expired token
Then the response has HTTP status 401
And the response JSON should have "message" containing "Invalid token"
Scenario: Confirmation fails when token is missing
Given the API is running
And I have registered a new account
And I have a valid access token for my account
When I submit a confirmation request with a missing token
Then the response has HTTP status 400
Scenario: Confirmation fails with tampered token (wrong secret)
Given the API is running
And I have registered a new account
And I have a confirmation token signed with the wrong secret
And I have a valid access token for my account
When I submit a confirmation request with the tampered token
Then the response has HTTP status 401
And the response JSON should have "message" containing "Invalid token"
Scenario: Confirmation endpoint only accepts POST requests
Given the API is running
And I have a valid confirmation token
When I submit a confirmation request using an invalid HTTP method
Then the response has HTTP status 404
Scenario: Confirmation fails when token is missing
Given the API is running
And I have registered a new account
And I have a valid access token for my account
When I submit a confirmation request with a missing token
Then the response has HTTP status 400
Scenario: Confirmation fails with malformed token
Given the API is running
And I have registered a new account
And I have a valid access token for my account
When I submit a confirmation request with a malformed token
Then the response has HTTP status 401
And the response JSON should have "message" containing "Invalid token"
Scenario: Confirmation endpoint only accepts POST requests
Given the API is running
And I have a valid confirmation token
When I submit a confirmation request using an invalid HTTP method
Then the response has HTTP status 404
Scenario: Confirmation fails without an access token
Given the API is running
And I have registered a new account
And I have a valid confirmation token for my account
When I submit a confirmation request with the valid token without an access token
Then the response has HTTP status 401
Scenario: Confirmation fails with malformed token
Given the API is running
And I have registered a new account
And I have a valid access token for my account
When I submit a confirmation request with a malformed token
Then the response has HTTP status 401
And the response JSON should have "message" containing "Invalid token"
Scenario: Confirmation fails without an access token
Given the API is running
And I have registered a new account
And I have a valid confirmation token for my account
When I submit a confirmation request with the valid token without an access token
Then the response has HTTP status 401

View File

@@ -3,37 +3,37 @@ As a registered user
I want to log in to my account
So that I receive an authentication token to access authenticated routes
Scenario: Successful login with valid credentials
Given the API is running
And I have an existing account
When I submit a login request with a username and password
Then the response has HTTP status 200
And the response JSON should have "message" equal "Logged in successfully."
And the response JSON should have an access token
Scenario: Successful login with valid credentials
Given the API is running
And I have an existing account
When I submit a login request with a username and password
Then the response has HTTP status 200
And the response JSON should have "message" equal "Logged in successfully."
And the response JSON should have an access token
Scenario: Login fails with invalid credentials
Given the API is running
And I do not have an existing account
When I submit a login request with a username and password
Then the response has HTTP status 401
And the response JSON should have "message" equal "Invalid username or password."
Scenario: Login fails with invalid credentials
Given the API is running
And I do not have an existing account
When I submit a login request with a username and password
Then the response has HTTP status 401
And the response JSON should have "message" equal "Invalid username or password."
Scenario: Login fails when required missing username
Given the API is running
When I submit a login request with a missing username
Then the response has HTTP status 400
Scenario: Login fails when required missing username
Given the API is running
When I submit a login request with a missing username
Then the response has HTTP status 400
Scenario: Login fails when required missing password
Given the API is running
When I submit a login request with a missing password
Then the response has HTTP status 400
Scenario: Login fails when required missing password
Given the API is running
When I submit a login request with a missing password
Then the response has HTTP status 400
Scenario: Login fails when both username and password are missing
Given the API is running
When I submit a login request with both username and password missing
Then the response has HTTP status 400
Scenario: Login fails when both username and password are missing
Given the API is running
When I submit a login request with both username and password missing
Then the response has HTTP status 400
Scenario: Login endpoint only accepts POST requests
Given the API is running
When I submit a login request using a GET request
Then the response has HTTP status 404
Scenario: Login endpoint only accepts POST requests
Given the API is running
When I submit a login request using a GET request
Then the response has HTTP status 404

View File

@@ -3,8 +3,8 @@ As a client of the API
I want consistent 404 responses
So that consumers can gracefully handle missing routes
Scenario: GET request to an invalid route returns 404
Given the API is running
When I send an HTTP request "GET" to "/invalid-route"
Then the response has HTTP status 404
And the response JSON should have "message" equal "Route not found."
Scenario: GET request to an invalid route returns 404
Given the API is running
When I send an HTTP request "GET" to "/invalid-route"
Then the response has HTTP status 404
And the response JSON should have "message" equal "Route not found."

View File

@@ -1,60 +1,60 @@
Feature: User Registration
As a new user
I want to register an account
So that I can log in and access authenticated routes
As a new user
I want to register an account
So that I can log in and access authenticated routes
Scenario: Successful registration with valid details
Scenario: Successful registration with valid details
Given the API is running
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| newuser | New | User | newuser@example.com | 1990-01-01 | Password1! |
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| newuser | New | User | newuser@example.com | 1990-01-01 | Password1! |
Then the response has HTTP status 201
And the response JSON should have "message" equal "User registered successfully."
And the response JSON should have an access token
Scenario: Registration fails with existing username
Scenario: Registration fails with existing username
Given the API is running
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| test.user | Test | User | example@example.com | 2001-11-11 | Password1! |
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| test.user | Test | User | example@example.com | 2001-11-11 | Password1! |
Then the response has HTTP status 409
Scenario: Registration fails with existing email
Scenario: Registration fails with existing email
Given the API is running
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| newuser | New | User | test.user@thebiergarten.app | 1990-01-01 | Password1! |
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| newuser | New | User | test.user@thebiergarten.app | 1990-01-01 | Password1! |
Then the response has HTTP status 409
Scenario: Registration fails with missing required fields
Scenario: Registration fails with missing required fields
Given the API is running
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| | New | User | | | Password1! |
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| | New | User | | | Password1! |
Then the response has HTTP status 400
Scenario: Registration fails with invalid email format
Scenario: Registration fails with invalid email format
Given the API is running
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| newuser | New | User | invalidemail | 1990-01-01 | Password1! |
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| newuser | New | User | invalidemail | 1990-01-01 | Password1! |
Then the response has HTTP status 400
Scenario: Registration fails with weak password
Scenario: Registration fails with weak password
Given the API is running
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| newuser | New | User | newuser@example.com | 1990-01-01 | weakpass |
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| newuser | New | User | newuser@example.com | 1990-01-01 | weakpass |
Then the response has HTTP status 400
Scenario: Cannot register a user younger than 19 years of age (regulatory requirement)
Scenario: Cannot register a user younger than 19 years of age (regulatory requirement)
Given the API is running
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| younguser | Young | User | younguser@example.com | {underage_date} | Password1! |
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| younguser | Young | User | younguser@example.com | {underage_date} | Password1! |
Then the response has HTTP status 400
Scenario: Registration endpoint only accepts POST requests
Scenario: Registration endpoint only accepts POST requests
Given the API is running
When I submit a registration request using a GET request
Then the response has HTTP status 404
Then the response has HTTP status 404

View File

@@ -1,36 +1,36 @@
Feature: Resend Confirmation Email
As a user who did not receive the confirmation email
I want to request a resend of the confirmation email
So that I can obtain a working confirmation link while preventing abuse
As a user who did not receive the confirmation email
I want to request a resend of the confirmation email
So that I can obtain a working confirmation link while preventing abuse
Scenario: Legitimate resend for an unconfirmed user
Given the API is running
And I have registered a new account
And I have a valid access token for my account
When I submit a resend confirmation request for my account
Then the response has HTTP status 200
And the response JSON should have "message" containing "confirmation email has been resent"
Scenario: Legitimate resend for an unconfirmed user
Given the API is running
And I have registered a new account
And I have a valid access token for my account
When I submit a resend confirmation request for my account
Then the response has HTTP status 200
And the response JSON should have "message" containing "confirmation email has been resent"
Scenario: Resend is a no-op for an already confirmed user
Given the API is running
And I have registered a new account
And I have a valid confirmation token for my account
And I have a valid access token for my account
And I have confirmed my account
When I submit a resend confirmation request for my account
Then the response has HTTP status 200
And the response JSON should have "message" containing "confirmation email has been resent"
Scenario: Resend is a no-op for an already confirmed user
Given the API is running
And I have registered a new account
And I have a valid confirmation token for my account
And I have a valid access token for my account
And I have confirmed my account
When I submit a resend confirmation request for my account
Then the response has HTTP status 200
And the response JSON should have "message" containing "confirmation email has been resent"
Scenario: Resend is a no-op for a non-existent user
Given the API is running
And I have registered a new account
And I have a valid access token for my account
When I submit a resend confirmation request for a non-existent user
Then the response has HTTP status 200
And the response JSON should have "message" containing "confirmation email has been resent"
Scenario: Resend is a no-op for a non-existent user
Given the API is running
And I have registered a new account
And I have a valid access token for my account
When I submit a resend confirmation request for a non-existent user
Then the response has HTTP status 200
And the response JSON should have "message" containing "confirmation email has been resent"
Scenario: Resend requires authentication
Given the API is running
And I have registered a new account
When I submit a resend confirmation request without an access token
Then the response has HTTP status 401
Scenario: Resend requires authentication
Given the API is running
And I have registered a new account
When I submit a resend confirmation request without an access token
Then the response has HTTP status 401

View File

@@ -1,39 +1,39 @@
Feature: Token Refresh
As an authenticated user
I want to refresh my access token using my refresh token
So that I can maintain my session without logging in again
As an authenticated user
I want to refresh my access token using my refresh token
So that I can maintain my session without logging in again
Scenario: Successful token refresh with valid refresh token
Given the API is running
And I have an existing account
And I am logged in
When I submit a refresh token request with a valid refresh token
Then the response has HTTP status 200
And the response JSON should have "message" equal "Token refreshed successfully."
And the response JSON should have a new access token
And the response JSON should have a new refresh token
Scenario: Successful token refresh with valid refresh token
Given the API is running
And I have an existing account
And I am logged in
When I submit a refresh token request with a valid refresh token
Then the response has HTTP status 200
And the response JSON should have "message" equal "Token refreshed successfully."
And the response JSON should have a new access token
And the response JSON should have a new refresh token
Scenario: Token refresh fails with invalid refresh token
Given the API is running
When I submit a refresh token request with an invalid refresh token
Then the response has HTTP status 401
And the response JSON should have "message" containing "Invalid"
Scenario: Token refresh fails with invalid refresh token
Given the API is running
When I submit a refresh token request with an invalid refresh token
Then the response has HTTP status 401
And the response JSON should have "message" containing "Invalid"
Scenario: Token refresh fails with expired refresh token
Given the API is running
And I have an existing account
And I am logged in with an immediately-expiring refresh token
When I submit a refresh token request with the expired refresh token
Then the response has HTTP status 401
And the response JSON should have "message" containing "Invalid token"
Scenario: Token refresh fails with expired refresh token
Given the API is running
And I have an existing account
And I am logged in with an immediately-expiring refresh token
When I submit a refresh token request with the expired refresh token
Then the response has HTTP status 401
And the response JSON should have "message" containing "Invalid token"
Scenario: Token refresh fails when refresh token is missing
Given the API is running
When I submit a refresh token request with a missing refresh token
Then the response has HTTP status 400
Scenario: Token refresh fails when refresh token is missing
Given the API is running
When I submit a refresh token request with a missing refresh token
Then the response has HTTP status 400
Scenario: Token refresh endpoint only accepts POST requests
Given the API is running
And I have a valid refresh token
When I submit a refresh token request using a GET request
Then the response has HTTP status 404
Scenario: Token refresh endpoint only accepts POST requests
Given the API is running
And I have a valid refresh token
When I submit a refresh token request using a GET request
Then the response has HTTP status 404

View File

@@ -20,7 +20,7 @@ public class MockEmailDispatcher : IEmailDispatcher
FirstName = firstName,
Email = email,
ConfirmationToken = confirmationToken,
SentAt = DateTime.UtcNow,
SentAt = DateTime.UtcNow
}
);
@@ -39,7 +39,7 @@ public class MockEmailDispatcher : IEmailDispatcher
FirstName = firstName,
Email = email,
ConfirmationToken = confirmationToken,
SentAt = DateTime.UtcNow,
SentAt = DateTime.UtcNow
}
);
@@ -67,4 +67,4 @@ public class MockEmailDispatcher : IEmailDispatcher
public string ConfirmationToken { get; init; } = string.Empty;
public DateTime SentAt { get; init; }
}
}
}

View File

@@ -3,8 +3,8 @@ using Infrastructure.Email;
namespace API.Specs.Mocks;
/// <summary>
/// Mock email provider for testing that doesn't actually send emails.
/// Tracks sent emails for verification in tests if needed.
/// Mock email provider for testing that doesn't actually send emails.
/// Tracks sent emails for verification in tests if needed.
/// </summary>
public class MockEmailProvider : IEmailProvider
{
@@ -24,7 +24,7 @@ public class MockEmailProvider : IEmailProvider
Subject = subject,
Body = body,
IsHtml = isHtml,
SentAt = DateTime.UtcNow,
SentAt = DateTime.UtcNow
}
);
@@ -45,7 +45,7 @@ public class MockEmailProvider : IEmailProvider
Subject = subject,
Body = body,
IsHtml = isHtml,
SentAt = DateTime.UtcNow,
SentAt = DateTime.UtcNow
}
);
@@ -65,4 +65,4 @@ public class MockEmailProvider : IEmailProvider
public bool IsHtml { get; init; }
public DateTime SentAt { get; init; }
}
}
}

View File

@@ -1,5 +1,5 @@
using System.Text;
using System.Text.Json;
using API.Specs;
using FluentAssertions;
using Reqnroll;
@@ -15,14 +15,11 @@ public class ApiGeneralSteps(ScenarioContext scenario)
private HttpClient GetClient()
{
if (scenario.TryGetValue<HttpClient>(ClientKey, out var client))
{
return client;
}
if (scenario.TryGetValue<HttpClient>(ClientKey, out HttpClient? client)) return client;
var factory = scenario.TryGetValue<TestApiFactory>(
TestApiFactory? factory = scenario.TryGetValue<TestApiFactory>(
FactoryKey,
out var f
out TestApiFactory? f
)
? f
: new TestApiFactory();
@@ -46,19 +43,19 @@ public class ApiGeneralSteps(ScenarioContext scenario)
string jsonBody
)
{
var client = GetClient();
HttpClient client = GetClient();
var requestMessage = new HttpRequestMessage(new HttpMethod(method), url)
HttpRequestMessage requestMessage = new(new HttpMethod(method), url)
{
Content = new StringContent(
jsonBody,
System.Text.Encoding.UTF8,
Encoding.UTF8,
"application/json"
),
)
};
var response = await client.SendAsync(requestMessage);
var responseBody = await response.Content.ReadAsStringAsync();
HttpResponseMessage response = await client.SendAsync(requestMessage);
string responseBody = await response.Content.ReadAsStringAsync();
scenario[ResponseKey] = response;
scenario[ResponseBodyKey] = responseBody;
@@ -70,13 +67,13 @@ public class ApiGeneralSteps(ScenarioContext scenario)
string url
)
{
var client = GetClient();
var requestMessage = new HttpRequestMessage(
HttpClient client = GetClient();
HttpRequestMessage requestMessage = new(
new HttpMethod(method),
url
);
var response = await client.SendAsync(requestMessage);
var responseBody = await response.Content.ReadAsStringAsync();
HttpResponseMessage response = await client.SendAsync(requestMessage);
string responseBody = await response.Content.ReadAsStringAsync();
scenario[ResponseKey] = response;
scenario[ResponseBodyKey] = responseBody;
@@ -86,7 +83,7 @@ public class ApiGeneralSteps(ScenarioContext scenario)
public void ThenTheResponseStatusCodeShouldBeInt(int expected)
{
scenario
.TryGetValue<HttpResponseMessage>(ResponseKey, out var response)
.TryGetValue<HttpResponseMessage>(ResponseKey, out HttpResponseMessage? response)
.Should()
.BeTrue();
((int)response!.StatusCode).Should().Be(expected);
@@ -96,7 +93,7 @@ public class ApiGeneralSteps(ScenarioContext scenario)
public void ThenTheResponseHasHttpStatusInt(int expectedCode)
{
scenario
.TryGetValue<HttpResponseMessage>(ResponseKey, out var response)
.TryGetValue<HttpResponseMessage>(ResponseKey, out HttpResponseMessage? response)
.Should()
.BeTrue("No response was received from the API");
((int)response!.StatusCode).Should().Be(expectedCode);
@@ -109,20 +106,20 @@ public class ApiGeneralSteps(ScenarioContext scenario)
)
{
scenario
.TryGetValue<HttpResponseMessage>(ResponseKey, out var response)
.TryGetValue<HttpResponseMessage>(ResponseKey, out HttpResponseMessage? response)
.Should()
.BeTrue();
scenario
.TryGetValue<string>(ResponseBodyKey, out var responseBody)
.TryGetValue<string>(ResponseBodyKey, out string? responseBody)
.Should()
.BeTrue();
using var doc = JsonDocument.Parse(responseBody!);
var root = doc.RootElement;
using JsonDocument doc = JsonDocument.Parse(responseBody!);
JsonElement root = doc.RootElement;
if (!root.TryGetProperty(field, out var value))
if (!root.TryGetProperty(field, out JsonElement value))
{
root.TryGetProperty("payload", out var payloadElem)
root.TryGetProperty("payload", out JsonElement payloadElem)
.Should()
.BeTrue(
"Expected field '{0}' to be present either at the root or inside 'payload'",
@@ -157,20 +154,20 @@ public class ApiGeneralSteps(ScenarioContext scenario)
)
{
scenario
.TryGetValue<HttpResponseMessage>(ResponseKey, out var response)
.TryGetValue<HttpResponseMessage>(ResponseKey, out HttpResponseMessage? response)
.Should()
.BeTrue();
scenario
.TryGetValue<string>(ResponseBodyKey, out var responseBody)
.TryGetValue<string>(ResponseBodyKey, out string? responseBody)
.Should()
.BeTrue();
using var doc = JsonDocument.Parse(responseBody!);
var root = doc.RootElement;
using JsonDocument doc = JsonDocument.Parse(responseBody!);
JsonElement root = doc.RootElement;
if (!root.TryGetProperty(field, out var value))
if (!root.TryGetProperty(field, out JsonElement value))
{
root.TryGetProperty("payload", out var payloadElem)
root.TryGetProperty("payload", out JsonElement payloadElem)
.Should()
.BeTrue(
"Expected field '{0}' to be present either at the root or inside 'payload'",
@@ -195,7 +192,7 @@ public class ApiGeneralSteps(ScenarioContext scenario)
"Expected field '{0}' to be a string",
field
);
var actualValue = value.GetString();
string? actualValue = value.GetString();
actualValue
.Should()
.Contain(
@@ -206,4 +203,4 @@ public class ApiGeneralSteps(ScenarioContext scenario)
actualValue
);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +1,37 @@
using System.Collections.Generic;
using API.Specs.Mocks;
using Features.Emails.Services;
using Infrastructure.Email;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace API.Specs
namespace API.Specs;
public class TestApiFactory : WebApplicationFactory<Program>
{
public class TestApiFactory : WebApplicationFactory<Program>
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
builder.UseEnvironment("Testing");
builder.ConfigureServices(services =>
{
builder.UseEnvironment("Testing");
// Replace the real email provider with mock for testing
ServiceDescriptor? emailProviderDescriptor = services.SingleOrDefault(d =>
d.ServiceType == typeof(IEmailProvider)
);
builder.ConfigureServices(services =>
{
// Replace the real email provider with mock for testing
var emailProviderDescriptor = services.SingleOrDefault(d =>
d.ServiceType == typeof(IEmailProvider)
);
if (emailProviderDescriptor != null) services.Remove(emailProviderDescriptor);
if (emailProviderDescriptor != null)
{
services.Remove(emailProviderDescriptor);
}
services.AddScoped<IEmailProvider, MockEmailProvider>();
services.AddScoped<IEmailProvider, MockEmailProvider>();
// Replace the real email dispatcher with mock for testing
ServiceDescriptor? emailDispatcherDescriptor = services.SingleOrDefault(d =>
d.ServiceType == typeof(IEmailDispatcher)
);
// Replace the real email dispatcher with mock for testing
var emailDispatcherDescriptor = services.SingleOrDefault(d =>
d.ServiceType == typeof(IEmailDispatcher)
);
if (emailDispatcherDescriptor != null) services.Remove(emailDispatcherDescriptor);
if (emailDispatcherDescriptor != null)
{
services.Remove(emailDispatcherDescriptor);
}
services.AddScoped<IEmailDispatcher, MockEmailDispatcher>();
});
}
services.AddScoped<IEmailDispatcher, MockEmailDispatcher>();
});
}
}
}