elsa-core/test/integration/Elsa.ExternalAuthentication.IntegrationTests/Compatibility/LegacyIdentityEndpointTests.cs
2026-07-24 19:04:26 +02:00

136 lines
6.2 KiB
C#

using System.Net;
using System.Net.Http.Json;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using Elsa.Identity.Contracts;
using Elsa.Identity.Constants;
using Elsa.Identity.Entities;
using Elsa.Identity.Models;
using FastEndpoints;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NSubstitute;
namespace Elsa.ExternalAuthentication.IntegrationTests.Compatibility;
/// <summary>
/// Protects the existing direct local-credential contracts while the broker-local flow remains additive.
/// </summary>
public sealed class LegacyIdentityEndpointTests : IAsyncLifetime
{
private readonly IUserCredentialsValidator _credentialsValidator = Substitute.For<IUserCredentialsValidator>();
private readonly IUserProvider _userProvider = Substitute.For<IUserProvider>();
private readonly IAccessTokenIssuer _tokenIssuer = Substitute.For<IAccessTokenIssuer>();
private WebApplication? _app;
private HttpClient? _client;
private bool _wasSecurityEnabled;
public async Task InitializeAsync()
{
_wasSecurityEnabled = EndpointSecurityOptions.SecurityIsEnabled;
EndpointSecurityOptions.SecurityIsEnabled = false;
var builder = WebApplication.CreateSlimBuilder();
builder.WebHost.UseTestServer();
builder.Services.AddFastEndpoints(options =>
{
options.Assemblies = [typeof(Elsa.Identity.Features.IdentityFeature).Assembly];
options.Filter = endpoint => endpoint.Namespace is "Elsa.Identity.Endpoints.Login" or "Elsa.Identity.Endpoints.RefreshToken";
});
builder.Services.AddSingleton(_credentialsValidator);
builder.Services.AddSingleton(_userProvider);
builder.Services.AddSingleton(_tokenIssuer);
builder.Services
.AddAuthentication()
.AddScheme<AuthenticationSchemeOptions, RefreshTokenAuthenticationHandler>(IdentityAuthenticationSchemes.RefreshToken, _ => { });
builder.Services.AddAuthorization();
_app = builder.Build();
_app.Use(async (context, next) =>
{
context.User = new ClaimsPrincipal(new ClaimsIdentity([new Claim(ClaimTypes.Name, "admin")], "legacy-refresh"));
await next(context);
});
_app.UseAuthorization();
_app.UseFastEndpoints();
await _app.StartAsync();
_client = _app.GetTestClient();
}
public async Task DisposeAsync()
{
EndpointSecurityOptions.SecurityIsEnabled = _wasSecurityEnabled;
_client?.Dispose();
if (_app is not null)
{
await _app.StopAsync();
await _app.DisposeAsync();
}
}
[Fact]
public async Task IdentityLoginRetainsItsRouteCredentialValidationAndResponseShape()
{
var user = new User { Id = "user-a", Name = "admin" };
_credentialsValidator.ValidateAsync("admin", "password", Arg.Any<CancellationToken>()).Returns(user);
_tokenIssuer.IssueTokensAsync(user, Arg.Any<CancellationToken>()).Returns(new IssuedTokens("access-a", "refresh-a"));
var response = await _client!.PostAsJsonAsync("/identity/login", new { username = " admin ", password = " password " });
var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.True(document.RootElement.GetProperty("isAuthenticated").GetBoolean());
Assert.Equal("access-a", document.RootElement.GetProperty("accessToken").GetString());
Assert.Equal("refresh-a", document.RootElement.GetProperty("refreshToken").GetString());
await _credentialsValidator.Received(1).ValidateAsync("admin", "password", Arg.Any<CancellationToken>());
}
[Fact]
public async Task IdentityLoginRetainsItsGenericUnauthenticatedResponse()
{
_credentialsValidator.ValidateAsync("unknown", "wrong", Arg.Any<CancellationToken>()).Returns((User?)null);
var response = await _client!.PostAsJsonAsync("/identity/login", new { username = "unknown", password = "wrong" });
var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.False(document.RootElement.GetProperty("isAuthenticated").GetBoolean());
Assert.Equal(JsonValueKind.Null, document.RootElement.GetProperty("accessToken").ValueKind);
Assert.Equal(JsonValueKind.Null, document.RootElement.GetProperty("refreshToken").ValueKind);
}
[Fact]
public async Task IdentityRefreshTokenRetainsItsRouteAndLocalTokenContract()
{
var user = new User { Id = "user-a", Name = "admin" };
_userProvider.FindAsync(Arg.Is<UserFilter>(filter => filter.Name == "admin"), Arg.Any<CancellationToken>()).Returns(user);
_tokenIssuer.IssueTokensAsync(user, Arg.Any<CancellationToken>()).Returns(new IssuedTokens("access-b", "refresh-b"));
var response = await _client!.PostAsync("/identity/refresh-token", null);
var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.True(document.RootElement.GetProperty("isAuthenticated").GetBoolean());
Assert.Equal("access-b", document.RootElement.GetProperty("accessToken").GetString());
Assert.Equal("refresh-b", document.RootElement.GetProperty("refreshToken").GetString());
}
private sealed class RefreshTokenAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder) : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
{
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var identity = new ClaimsIdentity([new Claim(ClaimTypes.Name, "admin")], Scheme.Name);
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
}