mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-14 17:54:42 +00:00
refactor(api-core): move service registration (#275)
This commit is contained in:
@@ -123,7 +123,8 @@ own
|
|||||||
|
|
||||||
| Component | Description |
|
| Component | Description |
|
||||||
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `Program.cs` | Registers MediatR (scanning every `Features.*` assembly), FluentValidation, and uses `AddApplicationPart` so each slice's controllers are discovered by MVC. |
|
| `Program.cs` | Thin composition root: builds the host and calls the `ServiceCollectionExtensions` methods below. |
|
||||||
|
| `ServiceCollectionExtensions.cs` | Registers MediatR (scanning every `Features.*` assembly), FluentValidation, and uses `AddApplicationPart` so each slice's controllers are discovered by MVC. |
|
||||||
| `GlobalException.cs` | Global exception filter (host-level cross-cutting concern). |
|
| `GlobalException.cs` | Global exception filter (host-level cross-cutting concern). |
|
||||||
| `Authentication/JwtAuthenticationHandler.cs` | JWT auth scheme middleware. |
|
| `Authentication/JwtAuthenticationHandler.cs` | JWT auth scheme middleware. |
|
||||||
| Swagger/OpenAPI documentation, health check endpoints | Host-level documentation and health endpoints. |
|
| Swagger/OpenAPI documentation, health check endpoints | Host-level documentation and health endpoints. |
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ directly, bypassing the `Features.Users` slice entirely.
|
|||||||
`TokenService` is registered by `Features.Users`' own `AddFeaturesUsers()`
|
`TokenService` is registered by `Features.Users`' own `AddFeaturesUsers()`
|
||||||
extension method, except for the lower-level `ITokenInfrastructure` (JWT
|
extension method, except for the lower-level `ITokenInfrastructure` (JWT
|
||||||
signing/verification) it depends on, which is registered by the host
|
signing/verification) it depends on, which is registered by the host
|
||||||
(`API.Core/Program.cs`) since `JwtAuthenticationHandler` (host-level auth
|
(`API.Core/ServiceCollectionExtensions.cs`, via `AddCoreInfrastructure()`,
|
||||||
|
called from `Program.cs`) since `JwtAuthenticationHandler` (host-level auth
|
||||||
middleware) also depends on it directly.
|
middleware) also depends on it directly.
|
||||||
|
|
||||||
### Integration points
|
### Integration points
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="API.Specs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.11" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.11" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||||
|
|||||||
@@ -25,17 +25,13 @@ public class JwtAuthenticationHandler(
|
|||||||
/// 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>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The signing secret is resolved first from the <c>ACCESS_TOKEN_SECRET</c> environment variable, falling
|
/// The signing secret is resolved first from the <c>ACCESS_TOKEN_SECRET</c> environment variable.
|
||||||
/// back to the <c>Jwt:SecretKey</c> configuration value, to stay consistent with the secret source used
|
/// Fails if the secret is not configured, the <c>Authorization</c> header is
|
||||||
/// when tokens are issued. Fails if the secret is not configured, the <c>Authorization</c> header is
|
|
||||||
/// missing or not <c>Bearer</c>-scheme, or the token fails validation.
|
/// missing or not <c>Bearer</c>-scheme, or the token fails validation.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
{
|
{
|
||||||
// Use the same access-token secret source as TokenService to avoid mismatched validation.
|
|
||||||
string? secret = configuration[ConfigurationKeys.AccessTokenSecret];
|
string? secret = configuration[ConfigurationKeys.AccessTokenSecret];
|
||||||
if (string.IsNullOrWhiteSpace(secret))
|
|
||||||
secret = configuration[ConfigurationKeys.JwtSecretKeyFallback];
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(secret))
|
if (string.IsNullOrWhiteSpace(secret))
|
||||||
return AuthenticateResult.Fail("JWT secret is not configured");
|
return AuthenticateResult.Fail("JWT secret is not configured");
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace API.Core.Controllers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles requests that do not match any other route, used as the application's fallback controller.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// 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")]
|
|
||||||
public class NotFoundController : ControllerBase
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a 404 response for any request that did not match a route.
|
|
||||||
/// </summary>
|
|
||||||
[HttpGet("404")]
|
|
||||||
public IActionResult Handle404()
|
|
||||||
{
|
|
||||||
return NotFound(new { message = "Route not found." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Shared.Contracts;
|
|
||||||
using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames;
|
|
||||||
|
|
||||||
namespace API.Core.Controllers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sample controller demonstrating an endpoint that requires JWT authentication.
|
|
||||||
/// </summary>
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
[Authorize(AuthenticationSchemes = "JWT")]
|
|
||||||
public class ProtectedController : ControllerBase
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the authenticated caller's user ID and username extracted from the JWT claims.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns><c>userId</c> and <c>username</c> may be <c>null</c> if not present in the token.</returns>
|
|
||||||
[HttpGet]
|
|
||||||
public ActionResult<ResponseBody<object>> Get()
|
|
||||||
{
|
|
||||||
string? userId = User.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;
|
|
||||||
string? username = User.FindFirst(JwtRegisteredClaimNames.UniqueName)?.Value;
|
|
||||||
|
|
||||||
return Ok(
|
|
||||||
new ResponseBody<object>
|
|
||||||
{
|
|
||||||
Message = "Protected endpoint accessed successfully",
|
|
||||||
Payload = new { userId, username },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,16 +13,16 @@ namespace API.Core;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger) : IExceptionFilter
|
public class GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger) : IExceptionFilter
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Logs the exception and sets <see cref="ExceptionContext.Result" /> to an appropriate error response.
|
/// Logs the exception and sets <see cref="ExceptionContext.Result" /> to an appropriate error response.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Maps <see cref="ValidationException" /> to 400, <see cref="ConflictException" /> to 409,
|
/// Maps <see cref="ValidationException" /> to 400, <see cref="ConflictException" /> to 409,
|
||||||
/// <see cref="NotFoundException" /> to 404, <see cref="UnauthorizedException" /> to 401,
|
/// <see cref="NotFoundException" /> to 404, <see cref="UnauthorizedException" /> to 401,
|
||||||
/// <see cref="ForbiddenException" /> to 403, <see cref="SqlException" /> to 503,
|
/// <see cref="ForbiddenException" /> to 403, <see cref="SqlException" /> to 503,
|
||||||
/// <see cref="Domain.Exceptions.ValidationException" /> to 400, and anything else to 500.
|
/// <see cref="Domain.Exceptions.ValidationException" /> to 400, and anything else to 500.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public void OnException(ExceptionContext context)
|
public void OnException(ExceptionContext context)
|
||||||
{
|
{
|
||||||
logger.LogError(context.Exception, "Unhandled exception occurred");
|
logger.LogError(context.Exception, "Unhandled exception occurred");
|
||||||
|
|
||||||
|
|||||||
@@ -1,132 +1,86 @@
|
|||||||
using API.Core;
|
|
||||||
using API.Core.Authentication;
|
|
||||||
using Database.Connection.DependencyInjection;
|
|
||||||
using Features.Auth.Controllers;
|
|
||||||
using Features.Users.DependencyInjection;
|
|
||||||
using Features.Breweries.Controllers;
|
|
||||||
using Features.Breweries.DependencyInjection;
|
|
||||||
using Features.Emails.DependencyInjection;
|
|
||||||
using Features.Emails.Services;
|
|
||||||
using Features.ImageUploads.Commands.UploadPhoto;
|
|
||||||
using Features.ImageUploads.DependencyInjection;
|
|
||||||
using FluentValidation;
|
|
||||||
using FluentValidation.AspNetCore;
|
|
||||||
using Infrastructure.FileUpload;
|
|
||||||
using Infrastructure.Jwt;
|
|
||||||
using MediatR;
|
|
||||||
using Microsoft.OpenApi.Models;
|
|
||||||
using Shared.Application.Behaviors;
|
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using Shared.Contracts;
|
||||||
using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames;
|
using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames;
|
||||||
|
|
||||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
namespace API.Core;
|
||||||
|
|
||||||
// Add current controllers to the exception filter
|
internal class Program
|
||||||
builder
|
|
||||||
.Services.AddControllers(options => { options.Filters.Add<GlobalExceptionFilter>(); })
|
|
||||||
.AddApplicationPart(typeof(BreweryController).Assembly)
|
|
||||||
.AddApplicationPart(typeof(AuthController).Assembly);
|
|
||||||
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
|
||||||
builder.Services.AddSwaggerGen(options =>
|
|
||||||
{
|
{
|
||||||
options.AddSecurityDefinition(
|
public static void Main(string[] args)
|
||||||
"Bearer",
|
{
|
||||||
new OpenApiSecurityScheme
|
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||||
{
|
|
||||||
Name = "Authorization",
|
builder.Logging.ClearProviders().AddConsole();
|
||||||
Type = SecuritySchemeType.Http,
|
|
||||||
Scheme = "Bearer",
|
if (!builder.Environment.IsProduction())
|
||||||
BearerFormat = "JWT",
|
builder.Logging.AddDebug();
|
||||||
In = ParameterLocation.Header,
|
|
||||||
Description = "Enter a JWT access token.",
|
builder
|
||||||
}
|
.Services.AddApiControllers()
|
||||||
);
|
.AddValidationAndMediatR()
|
||||||
options.AddSecurityRequirement(
|
.AddApplicationServices()
|
||||||
new OpenApiSecurityRequirement
|
.AddFeatureModules()
|
||||||
{
|
.AddCoreInfrastructure()
|
||||||
{
|
.AddJwtAuthentication();
|
||||||
new OpenApiSecurityScheme
|
|
||||||
|
WebApplication app = builder.Build();
|
||||||
|
|
||||||
|
app.UseSwagger()
|
||||||
|
.UseSwaggerUI()
|
||||||
|
.UseHttpsRedirection()
|
||||||
|
.UseAuthentication()
|
||||||
|
.UseAuthorization();
|
||||||
|
|
||||||
|
app.MapOpenApi();
|
||||||
|
app.MapHealthChecks("/health");
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
if (app.Environment.IsEnvironment("Testing"))
|
||||||
|
app.MapGet(
|
||||||
|
"/api/protected",
|
||||||
|
(ClaimsPrincipal user) =>
|
||||||
{
|
{
|
||||||
Reference = new OpenApiReference
|
string? userId = user.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;
|
||||||
{
|
string? username = user.FindFirst(JwtRegisteredClaimNames.UniqueName)?.Value;
|
||||||
Type = ReferenceType.SecurityScheme,
|
|
||||||
Id = "Bearer",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
builder.Services.AddOpenApi();
|
|
||||||
|
|
||||||
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
|
return Results.Ok(
|
||||||
builder.Services.AddValidatorsFromAssembly(typeof(BreweryController).Assembly);
|
new ResponseBody<object>
|
||||||
builder.Services.AddValidatorsFromAssembly(typeof(AuthController).Assembly);
|
{
|
||||||
builder.Services.AddValidatorsFromAssembly(typeof(UploadPhotoCommand).Assembly);
|
Message = "Protected endpoint accessed successfully",
|
||||||
builder.Services.AddFluentValidationAutoValidation();
|
Payload = new { userId, username },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
).RequireAuthorization(policy => policy.AddAuthenticationSchemes("JWT").RequireAuthenticatedUser());
|
||||||
|
|
||||||
// Add MediatR.
|
// Easter egg, as per tradition with the previous versions of the Biergarten App.
|
||||||
// ValidationBehavior runs FluentValidation validators in the MediatR pipeline for command/query handlers.
|
app.Map(
|
||||||
builder.Services.AddMediatR(cfg =>
|
"/teapot",
|
||||||
{
|
() =>
|
||||||
cfg.RegisterServicesFromAssemblyContaining<Program>();
|
Results.Json(
|
||||||
cfg.RegisterServicesFromAssembly(typeof(BreweryController).Assembly);
|
new ResponseBody
|
||||||
cfg.RegisterServicesFromAssembly(typeof(AuthController).Assembly);
|
{
|
||||||
cfg.RegisterServicesFromAssembly(typeof(UploadPhotoCommand).Assembly);
|
Message =
|
||||||
cfg.RegisterServicesFromAssembly(typeof(IEmailDispatcher).Assembly);
|
"I'm a little teapot, short and stout. Here is my handle, here is my spout!",
|
||||||
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
|
},
|
||||||
});
|
statusCode: 418
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.ExcludeFromDescription();
|
||||||
|
|
||||||
builder.Services.AddHealthChecks();
|
app.MapFallback(() =>
|
||||||
|
Results.NotFound(
|
||||||
|
new ResponseBody { Message = "Are you lost? That route does not exist." }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.ExcludeFromDescription();
|
||||||
|
|
||||||
// Configure logging for container output
|
app.Services.GetRequiredService<IHostApplicationLifetime>()
|
||||||
builder.Logging.ClearProviders();
|
.ApplicationStopping.Register(() =>
|
||||||
builder.Logging.AddConsole();
|
{
|
||||||
if (!builder.Environment.IsProduction())
|
app.Logger.LogInformation("Application is shutting down gracefully...");
|
||||||
builder.Logging.AddDebug();
|
});
|
||||||
|
|
||||||
builder.Services.AddDatabaseConnection();
|
app.Run();
|
||||||
|
}
|
||||||
builder.Services.AddFeaturesBreweries();
|
}
|
||||||
builder.Services.AddFeaturesUsers();
|
|
||||||
builder.Services.AddFeaturesEmails();
|
|
||||||
builder.Services.AddFeaturesPhotoUpload();
|
|
||||||
|
|
||||||
// ITokenInfrastructure is registered here because JwtAuthenticationHandler depends on it directly.
|
|
||||||
builder.Services.AddScoped<ITokenInfrastructure, JwtInfrastructure>();
|
|
||||||
|
|
||||||
builder.Services.AddSingleton<IFileStorageProvider, S3FileStorageProvider>();
|
|
||||||
|
|
||||||
builder.Services.AddScoped<GlobalExceptionFilter>();
|
|
||||||
|
|
||||||
builder
|
|
||||||
.Services.AddAuthentication("JWT")
|
|
||||||
.AddScheme<JwtAuthenticationOptions, JwtAuthenticationHandler>("JWT", options => { });
|
|
||||||
|
|
||||||
builder.Services.AddAuthorization();
|
|
||||||
|
|
||||||
WebApplication app = builder.Build();
|
|
||||||
|
|
||||||
app.UseSwagger();
|
|
||||||
app.UseSwaggerUI();
|
|
||||||
app.MapOpenApi();
|
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
|
||||||
|
|
||||||
app.UseAuthentication();
|
|
||||||
app.UseAuthorization();
|
|
||||||
app.MapHealthChecks("/health");
|
|
||||||
|
|
||||||
app.MapControllers();
|
|
||||||
|
|
||||||
app.MapFallbackToController("Handle404", "NotFound");
|
|
||||||
|
|
||||||
IHostApplicationLifetime lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
|
|
||||||
lifetime.ApplicationStopping.Register(() =>
|
|
||||||
{
|
|
||||||
app.Logger.LogInformation("Application is shutting down gracefully...");
|
|
||||||
});
|
|
||||||
|
|
||||||
app.Run();
|
|
||||||
|
|||||||
121
web/backend/API/API.Core/ServiceCollectionExtensions.cs
Normal file
121
web/backend/API/API.Core/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
using API.Core.Authentication;
|
||||||
|
using Database.Connection.DependencyInjection;
|
||||||
|
using Features.Auth.Controllers;
|
||||||
|
using Features.Breweries.Controllers;
|
||||||
|
using Features.Breweries.DependencyInjection;
|
||||||
|
using Features.Emails.DependencyInjection;
|
||||||
|
using Features.Emails.Services;
|
||||||
|
using Features.ImageUploads.Commands.UploadPhoto;
|
||||||
|
using Features.ImageUploads.DependencyInjection;
|
||||||
|
using Features.Users.DependencyInjection;
|
||||||
|
using FluentValidation;
|
||||||
|
using FluentValidation.AspNetCore;
|
||||||
|
using Infrastructure.FileUpload;
|
||||||
|
using Infrastructure.Jwt;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Shared.Application.Behaviors;
|
||||||
|
|
||||||
|
namespace API.Core;
|
||||||
|
|
||||||
|
internal static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
extension(IServiceCollection services)
|
||||||
|
{
|
||||||
|
public IServiceCollection AddApiControllers() =>
|
||||||
|
services
|
||||||
|
.AddControllers(options => options.Filters.Add<GlobalExceptionFilter>())
|
||||||
|
.AddApplicationPart(typeof(BreweryController).Assembly)
|
||||||
|
.AddApplicationPart(typeof(AuthController).Assembly)
|
||||||
|
.Services;
|
||||||
|
|
||||||
|
public IServiceCollection AddValidationAndMediatR()
|
||||||
|
{
|
||||||
|
services
|
||||||
|
.AddOpenApi()
|
||||||
|
.AddValidatorsFromAssemblyContaining<Program>()
|
||||||
|
.AddValidatorsFromAssembly(typeof(BreweryController).Assembly)
|
||||||
|
.AddValidatorsFromAssembly(typeof(AuthController).Assembly)
|
||||||
|
.AddValidatorsFromAssembly(typeof(UploadPhotoCommand).Assembly)
|
||||||
|
.AddFluentValidationAutoValidation()
|
||||||
|
.AddMediatR(cfg =>
|
||||||
|
{
|
||||||
|
cfg.RegisterServicesFromAssemblyContaining<Program>();
|
||||||
|
cfg.RegisterServicesFromAssembly(typeof(BreweryController).Assembly);
|
||||||
|
cfg.RegisterServicesFromAssembly(typeof(AuthController).Assembly);
|
||||||
|
cfg.RegisterServicesFromAssembly(typeof(UploadPhotoCommand).Assembly);
|
||||||
|
cfg.RegisterServicesFromAssembly(typeof(IEmailDispatcher).Assembly);
|
||||||
|
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
|
||||||
|
});
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IServiceCollection AddApplicationServices()
|
||||||
|
{
|
||||||
|
services
|
||||||
|
.AddEndpointsApiExplorer()
|
||||||
|
.AddSwaggerGen(options =>
|
||||||
|
{
|
||||||
|
options.AddSecurityDefinition(
|
||||||
|
"Bearer",
|
||||||
|
new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Name = "Authorization",
|
||||||
|
Type = SecuritySchemeType.Http,
|
||||||
|
Scheme = "Bearer",
|
||||||
|
BearerFormat = "JWT",
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
Description = "Enter a JWT access token.",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
options.AddSecurityRequirement(
|
||||||
|
new OpenApiSecurityRequirement
|
||||||
|
{
|
||||||
|
{
|
||||||
|
new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Reference = new OpenApiReference
|
||||||
|
{
|
||||||
|
Type = ReferenceType.SecurityScheme,
|
||||||
|
Id = "Bearer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddHealthChecks();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IServiceCollection AddFeatureModules() =>
|
||||||
|
services
|
||||||
|
.AddDatabaseConnection()
|
||||||
|
.AddFeaturesBreweries()
|
||||||
|
.AddFeaturesUsers()
|
||||||
|
.AddFeaturesEmails()
|
||||||
|
.AddFeaturesPhotoUpload();
|
||||||
|
|
||||||
|
public IServiceCollection AddCoreInfrastructure() =>
|
||||||
|
services
|
||||||
|
// ITokenInfrastructure is registered here because JwtAuthenticationHandler depends on it directly.
|
||||||
|
.AddScoped<ITokenInfrastructure, JwtInfrastructure>()
|
||||||
|
.AddSingleton<IFileStorageProvider, S3FileStorageProvider>()
|
||||||
|
.AddScoped<GlobalExceptionFilter>();
|
||||||
|
|
||||||
|
public IServiceCollection AddJwtAuthentication()
|
||||||
|
{
|
||||||
|
services
|
||||||
|
.AddAuthentication("JWT")
|
||||||
|
.AddScheme<JwtAuthenticationOptions, JwtAuthenticationHandler>(
|
||||||
|
"JWT",
|
||||||
|
options => { }
|
||||||
|
);
|
||||||
|
|
||||||
|
services.AddAuthorization();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,4 +7,4 @@ Scenario: GET request to an invalid route returns 404
|
|||||||
Given the API is running
|
Given the API is running
|
||||||
When I send an HTTP request "GET" to "/invalid-route"
|
When I send an HTTP request "GET" to "/invalid-route"
|
||||||
Then the response has HTTP status 404
|
Then the response has HTTP status 404
|
||||||
And the response JSON should have "message" equal "Route not found."
|
And the response JSON should have "message" equal "Are you lost? That route does not exist."
|
||||||
@@ -15,7 +15,7 @@ public abstract class ApiStepsBase(ScenarioContext scenario)
|
|||||||
protected const string ResponseKey = "response";
|
protected const string ResponseKey = "response";
|
||||||
protected const string ResponseBodyKey = "responseBody";
|
protected const string ResponseBodyKey = "responseBody";
|
||||||
|
|
||||||
protected TestApiFactory GetFactory()
|
internal TestApiFactory GetFactory()
|
||||||
{
|
{
|
||||||
TestApiFactory? factory = Scenario.TryGetValue<TestApiFactory>(
|
TestApiFactory? factory = Scenario.TryGetValue<TestApiFactory>(
|
||||||
FactoryKey,
|
FactoryKey,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using API.Core;
|
||||||
using API.Specs.Mocks;
|
using API.Specs.Mocks;
|
||||||
using Features.Emails.Services;
|
using Features.Emails.Services;
|
||||||
using Infrastructure.Email;
|
using Infrastructure.Email;
|
||||||
@@ -7,7 +8,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
|
|
||||||
namespace API.Specs;
|
namespace API.Specs;
|
||||||
|
|
||||||
public class TestApiFactory : WebApplicationFactory<Program>
|
internal class TestApiFactory : WebApplicationFactory<Program>
|
||||||
{
|
{
|
||||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user