tests: add remaining register integration tests
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
namespace FiscalOS.Infra.Authorization;
|
||||
|
||||
internal sealed class ProblemDetailsAuthResultHandler : IAuthorizationMiddlewareResultHandler
|
||||
{
|
||||
public Task HandleAsync(
|
||||
RequestDelegate next,
|
||||
HttpContext context,
|
||||
AuthorizationPolicy policy,
|
||||
PolicyAuthorizationResult authorizeResult
|
||||
)
|
||||
{
|
||||
if (authorizeResult.Succeeded)
|
||||
{
|
||||
return next(context);
|
||||
}
|
||||
|
||||
context.Response.StatusCode = authorizeResult.Forbidden
|
||||
? StatusCodes.Status403Forbidden
|
||||
: StatusCodes.Status401Unauthorized;
|
||||
|
||||
context.Response.ContentType = "application/problem+json";
|
||||
|
||||
var problemDetails = authorizeResult.Forbidden
|
||||
? new ProblemDetails
|
||||
{
|
||||
Title = "Forbidden",
|
||||
Status = StatusCodes.Status403Forbidden,
|
||||
Detail = "You do not have permission to access this resource."
|
||||
}
|
||||
: new ProblemDetails
|
||||
{
|
||||
Title = "Unauthorized",
|
||||
Status = StatusCodes.Status401Unauthorized,
|
||||
Detail = "Authentication is required to access this resource."
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(problemDetails);
|
||||
|
||||
return context.Response.WriteAsync(json);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddInfrastructure(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IAuthorizationMiddlewareResultHandler, ProblemDetailsAuthResultHandler>();
|
||||
|
||||
services.AddSingleton(TimeProvider.System);
|
||||
|
||||
services.ConfigureOptions<JwtOptionsSetup>();
|
||||
|
||||
@@ -2,13 +2,19 @@ global using System.IdentityModel.Tokens.Jwt;
|
||||
global using System.Security.Claims;
|
||||
global using System.Security.Cryptography;
|
||||
global using System.Text;
|
||||
global using System.Text.Json;
|
||||
|
||||
global using FiscalOS.Core.Authentication;
|
||||
global using FiscalOS.Core.Data;
|
||||
global using FiscalOS.Core.Identity;
|
||||
global using FiscalOS.Infra.Authentication;
|
||||
global using FiscalOS.Infra.Authorization;
|
||||
global using FiscalOS.Infra.Data;
|
||||
|
||||
global using Microsoft.AspNetCore.Authorization;
|
||||
global using Microsoft.AspNetCore.Authorization.Policy;
|
||||
global using Microsoft.AspNetCore.Http;
|
||||
global using Microsoft.AspNetCore.Mvc;
|
||||
global using Microsoft.EntityFrameworkCore;
|
||||
global using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
global using Microsoft.Extensions.Configuration;
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
namespace FiscalOS.API.Tests.Assertions;
|
||||
|
||||
internal static class HttpResponseExtensions
|
||||
{
|
||||
public static HttpResponseMessageAssertions Should(this HttpResponseMessage instance)
|
||||
{
|
||||
return new HttpResponseMessageAssertions(instance, AssertionChain.GetOrCreate());
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class HttpResponseMessageAssertions(
|
||||
HttpResponseMessage instance,
|
||||
AssertionChain assertionChain
|
||||
) : ReferenceTypeAssertions<HttpResponseMessage, HttpResponseMessageAssertions>(instance, assertionChain)
|
||||
{
|
||||
private readonly AssertionChain _chain = assertionChain;
|
||||
|
||||
protected override string Identifier => "HttpResponseMessage";
|
||||
|
||||
public async Task<AndWhichConstraint<HttpResponseMessageAssertions, T>> BeJsonContentOfType<T>(
|
||||
HttpStatusCode expectedStatusCode
|
||||
)
|
||||
{
|
||||
_chain.ForCondition(Subject.Content.Headers.ContentType?.MediaType is "application/json")
|
||||
.FailWith(
|
||||
"Expected response to be application/json, but found {0}",
|
||||
Subject.Content.Headers.ContentType?.MediaType
|
||||
);
|
||||
|
||||
_chain.ForCondition(Subject.StatusCode == expectedStatusCode)
|
||||
.FailWith(
|
||||
"Expected response status code to be {0}, but found {1}",
|
||||
expectedStatusCode,
|
||||
Subject.StatusCode
|
||||
);
|
||||
|
||||
var content = await Subject.Content.ReadFromJsonAsync<T>();
|
||||
|
||||
_chain.ForCondition(content is not null)
|
||||
.FailWith($"Expected body to be a valid {typeof(T).Name}, but it could not be deserialized.");
|
||||
|
||||
return new AndWhichConstraint<HttpResponseMessageAssertions, T>(this, content!);
|
||||
}
|
||||
|
||||
public AndConstraint<HttpResponseMessageAssertions> HaveSetCookieHeader(string cookieName)
|
||||
{
|
||||
_chain.ForCondition(Subject.Headers.TryGetValues("Set-Cookie", out var setCookieHeaders))
|
||||
.FailWith("Expected response to have 'Set-Cookie' header, but it was not found.");
|
||||
|
||||
var hasCookie = setCookieHeaders!.Any(header => header.StartsWith(cookieName + "=", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
_chain.ForCondition(hasCookie)
|
||||
.FailWith($"Expected response to have 'Set-Cookie' header for cookie '{cookieName}', but it was not found.");
|
||||
|
||||
return new AndConstraint<HttpResponseMessageAssertions>(this);
|
||||
}
|
||||
|
||||
public async Task<AndWhichConstraint<HttpResponseMessageAssertions, ProblemDetails>> BeProblemDetails(HttpStatusCode expectedStatusCode)
|
||||
{
|
||||
var problem = await ValidateAndDeserialize<ProblemDetails>(expectedStatusCode);
|
||||
return new AndWhichConstraint<HttpResponseMessageAssertions, ProblemDetails>(this, problem);
|
||||
}
|
||||
|
||||
public async Task<AndWhichConstraint<HttpResponseMessageAssertions, ValidationProblemDetails>> BeValidationProblemDetails(
|
||||
IDictionary<string, string[]> expectedErrors,
|
||||
HttpStatusCode expectedStatusCode = HttpStatusCode.BadRequest
|
||||
)
|
||||
{
|
||||
var problem = await ValidateAndDeserialize<ValidationProblemDetails>(expectedStatusCode);
|
||||
|
||||
_chain.ForCondition(problem.Errors is not null)
|
||||
.FailWith("Expected ValidationProblemDetails to contain errors, but the Errors dictionary was null.");
|
||||
|
||||
problem.Errors.Should().BeEquivalentTo(expectedErrors, "the validation errors should match the expected dictionary");
|
||||
|
||||
return new AndWhichConstraint<HttpResponseMessageAssertions, ValidationProblemDetails>(this, problem);
|
||||
}
|
||||
|
||||
private async Task<T> ValidateAndDeserialize<T>(HttpStatusCode expectedStatusCode) where T : ProblemDetails
|
||||
{
|
||||
_chain.ForCondition(Subject.Content.Headers.ContentType?.MediaType is "application/problem+json")
|
||||
.FailWith(
|
||||
"Expected response to be application/problem+json, but found {0}",
|
||||
Subject.Content.Headers.ContentType?.MediaType
|
||||
);
|
||||
|
||||
_chain.ForCondition(Subject.StatusCode == expectedStatusCode)
|
||||
.FailWith(
|
||||
"Expected response status code to be {0}, but found {1}",
|
||||
expectedStatusCode,
|
||||
Subject.StatusCode
|
||||
);
|
||||
|
||||
var problem = await Subject.Content.ReadFromJsonAsync<T>();
|
||||
|
||||
_chain.ForCondition(problem is not null)
|
||||
.FailWith($"Expected body to be a valid {typeof(T).Name}, but it could not be deserialized.");
|
||||
|
||||
return problem!;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ internal sealed class JwtTokenBuilder
|
||||
private const int ExpiryInMinutes = 5;
|
||||
private static readonly string Secret = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
||||
private readonly List<Claim> _claims = [];
|
||||
private DateTimeOffset _issuedAt = DateTimeOffset.UtcNow;
|
||||
private DateTimeOffset _expiresAt = DateTimeOffset.UtcNow.AddMinutes(ExpiryInMinutes);
|
||||
|
||||
public static JwtOptions DefaultJwtOptions => new()
|
||||
{
|
||||
@@ -28,22 +28,23 @@ internal sealed class JwtTokenBuilder
|
||||
return this;
|
||||
}
|
||||
|
||||
public JwtTokenBuilder IssuedAt(DateTimeOffset issuedAt)
|
||||
public JwtTokenBuilder WithExpiresAt(DateTimeOffset expiresAt)
|
||||
{
|
||||
_issuedAt = issuedAt;
|
||||
_expiresAt = expiresAt;
|
||||
return this;
|
||||
}
|
||||
|
||||
public string Build()
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var expiresAt = _issuedAt.AddMinutes(ExpiryInMinutes);
|
||||
var issuedAt = _expiresAt.AddMinutes(-ExpiryInMinutes);
|
||||
|
||||
var descriptor = new SecurityTokenDescriptor()
|
||||
{
|
||||
Subject = new(_claims),
|
||||
IssuedAt = _issuedAt.UtcDateTime,
|
||||
Expires = expiresAt.UtcDateTime,
|
||||
NotBefore = issuedAt.UtcDateTime,
|
||||
IssuedAt = issuedAt.UtcDateTime,
|
||||
Expires = _expiresAt.UtcDateTime,
|
||||
Issuer = Issuer,
|
||||
Audience = Audience,
|
||||
SigningCredentials = new(
|
||||
@@ -52,7 +53,7 @@ internal sealed class JwtTokenBuilder
|
||||
),
|
||||
};
|
||||
|
||||
var securityToken = tokenHandler.CreateJwtSecurityToken(descriptor);
|
||||
var securityToken = tokenHandler.CreateToken(descriptor);
|
||||
var jwtToken = tokenHandler.WriteToken(securityToken);
|
||||
|
||||
return jwtToken;
|
||||
|
||||
@@ -16,11 +16,7 @@ public class LoginTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
|
||||
var res = await Client.PostAsJsonAsync(LoginUri, req, TestContext.Current.CancellationToken);
|
||||
|
||||
res.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
|
||||
var problem = await res.Content.ReadFromJsonAsync<ValidationProblemDetails>(TestContext.Current.CancellationToken);
|
||||
|
||||
problem!.Errors.Should().BeEquivalentTo(tc.ExpectedErrors);
|
||||
await res.Should().BeValidationProblemDetails(tc.ExpectedErrors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -34,7 +30,7 @@ public class LoginTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
|
||||
var res = await Client.PostAsJsonAsync(LoginUri, req, TestContext.Current.CancellationToken);
|
||||
|
||||
res.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||
await res.Should().BeProblemDetails(HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -57,7 +53,7 @@ public class LoginTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
|
||||
var res = await Client.PostAsJsonAsync(LoginUri, req, TestContext.Current.CancellationToken);
|
||||
|
||||
res.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||
await res.Should().BeProblemDetails(HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -80,18 +76,8 @@ public class LoginTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
|
||||
var res = await Client.PostAsJsonAsync(LoginUri, req, TestContext.Current.CancellationToken);
|
||||
|
||||
res.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var content = await res.Content.ReadFromJsonAsync<Login.Response>(TestContext.Current.CancellationToken);
|
||||
|
||||
content!.AccessToken.Should().NotBeNullOrEmpty();
|
||||
|
||||
res.Headers.TryGetValues("Set-Cookie", out var cookies).Should().BeTrue();
|
||||
|
||||
var hasRefreshTokenCookie = cookies!
|
||||
.Any(static c => c.StartsWith("fiscalos_refresh_cookie=", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
hasRefreshTokenCookie.Should().BeTrue();
|
||||
res.Should().HaveSetCookieHeader("fiscalos_refresh_cookie");
|
||||
await res.Should().BeJsonContentOfType<Login.Response>(HttpStatusCode.OK);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
namespace FiscalOS.API.Tests.Integration;
|
||||
|
||||
public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
@@ -6,10 +5,189 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
private static readonly Uri RefreshUri = new("/refresh", UriKind.Relative);
|
||||
|
||||
[Fact]
|
||||
public async Task Refresh_WhenCalledWithNoAccessToken_ItShouldReturn401Unauthorized()
|
||||
public async Task Refresh_WhenCalledWithNoAccessToken_ItShouldReturn401WithProblemDetails()
|
||||
{
|
||||
var response = await Client.PostAsync(RefreshUri, null, TestContext.Current.CancellationToken); ;
|
||||
var response = await Client.PostAsync(RefreshUri, null, TestContext.Current.CancellationToken);
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||
await response.Should().BeProblemDetails(HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Refresh_WhenCalledWithNonExistentRefreshToken_ItShouldReturn400WithProblemDetails()
|
||||
{
|
||||
var jwt = JwtTokenBuilder.New()
|
||||
.WithClaim(JwtRegisteredClaimNames.Sub, Guid.NewGuid().ToString())
|
||||
.Build();
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, RefreshUri);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
||||
request.Headers.Add("Cookie", "fiscalos_refresh_cookie=nonexistenttoken");
|
||||
|
||||
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
await response.Should().BeProblemDetails(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Refresh_WhenCalledWithTokenBelongingToDifferentUser_ItShouldReturn403WithProblemDetails()
|
||||
{
|
||||
var (users, refreshToken) = await ExecuteDbContextAsync(static async (context, sp) =>
|
||||
{
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
||||
|
||||
var user1 = User.From("User1", passwordHasher.Hash("@Password1"));
|
||||
var user2 = User.From("User2", passwordHasher.Hash("@Password2"));
|
||||
var refreshToken1 = tokenGenerator.GenerateRefreshToken(user2);
|
||||
var refreshToken2 = tokenGenerator.GenerateRefreshToken(user2);
|
||||
user2.AddRefreshToken(refreshToken1);
|
||||
user2.AddRefreshToken(refreshToken2);
|
||||
|
||||
context.Add(user1);
|
||||
context.Add(user2);
|
||||
|
||||
await context.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
return (new User[] { user1, user2 }, refreshToken1);
|
||||
});
|
||||
|
||||
var jwt = JwtTokenBuilder.New()
|
||||
.WithClaim(JwtRegisteredClaimNames.Sub, users[0].Id.ToString())
|
||||
.Build();
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, RefreshUri);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
||||
request.Headers.Add("Cookie", $"fiscalos_refresh_cookie={refreshToken.Token}");
|
||||
|
||||
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
await response.Should().BeProblemDetails(HttpStatusCode.Forbidden);
|
||||
|
||||
var unrevokedTokensCountForUser2 = await ExecuteDbContextAsync(
|
||||
async context => await context.Set<RefreshToken>()
|
||||
.Include(t => t.User)
|
||||
.Where(t => t.UserId == users[1].Id && t.Revoked == false)
|
||||
.CountAsync()
|
||||
);
|
||||
|
||||
unrevokedTokensCountForUser2.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Refresh_WhenCalledWithRevokedRefreshToken_ItShouldReturn400WithProblemDetails()
|
||||
{
|
||||
var (user, refreshToken) = await ExecuteDbContextAsync(static async (context, sp) =>
|
||||
{
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
||||
|
||||
var user = User.From("User1", passwordHasher.Hash("@Password1"));
|
||||
var refreshToken = tokenGenerator.GenerateRefreshToken(user);
|
||||
refreshToken.Revoke();
|
||||
|
||||
user.AddRefreshToken(refreshToken);
|
||||
|
||||
context.Add(user);
|
||||
|
||||
await context.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
return (user, refreshToken);
|
||||
});
|
||||
|
||||
var jwt = JwtTokenBuilder.New()
|
||||
.WithClaim(JwtRegisteredClaimNames.Sub, user.Id.ToString())
|
||||
.Build();
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, RefreshUri);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
||||
request.Headers.Add("Cookie", $"fiscalos_refresh_cookie={refreshToken.Token}");
|
||||
|
||||
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
await response.Should().BeProblemDetails(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Refresh_WhenCalledWithExpiredRefreshToken_ItShouldReturn400WithProblemDetails()
|
||||
{
|
||||
var (user, refreshToken) = await ExecuteDbContextAsync(static async (context, sp) =>
|
||||
{
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
||||
var timeProvider = sp.GetRequiredService<TimeProvider>();
|
||||
|
||||
var user = User.From("User1", passwordHasher.Hash("@Password1"));
|
||||
var refreshToken = RefreshToken.From(user.Id, "expiredtoken", timeProvider.GetUtcNow().AddHours(-1));
|
||||
user.AddRefreshToken(refreshToken);
|
||||
|
||||
context.Add(user);
|
||||
|
||||
await context.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
return (user, refreshToken);
|
||||
});
|
||||
|
||||
var jwt = JwtTokenBuilder.New()
|
||||
.WithClaim(JwtRegisteredClaimNames.Sub, user.Id.ToString())
|
||||
.Build();
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, RefreshUri);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
||||
request.Headers.Add("Cookie", $"fiscalos_refresh_cookie={refreshToken.Token}");
|
||||
|
||||
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
await response.Should().BeProblemDetails(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(5)]
|
||||
[InlineData(-5)]
|
||||
public async Task Refresh_WhenCalledWithValidRefreshTokenAndExpiredOrNotExpiredAccessToken_ItShouldReturn200WithNewTokensAndSetRefreshCookie(int accessTokenExpiresAtOffset)
|
||||
{
|
||||
var (user, refreshToken) = await ExecuteDbContextAsync(static async (context, sp) =>
|
||||
{
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
||||
var timeProvider = sp.GetRequiredService<TimeProvider>();
|
||||
|
||||
var user = User.From("User1", passwordHasher.Hash("@Password1"));
|
||||
var refreshToken = tokenGenerator.GenerateRefreshToken(user);
|
||||
user.AddRefreshToken(refreshToken);
|
||||
|
||||
context.Add(user);
|
||||
|
||||
await context.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
return (user, refreshToken);
|
||||
});
|
||||
|
||||
var jwt = JwtTokenBuilder.New()
|
||||
.WithClaim(JwtRegisteredClaimNames.Sub, user.Id.ToString())
|
||||
.WithExpiresAt(DateTime.UtcNow.AddMinutes(accessTokenExpiresAtOffset))
|
||||
.Build();
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, RefreshUri);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
||||
request.Headers.Add("Cookie", $"fiscalos_refresh_cookie={refreshToken.Token}");
|
||||
|
||||
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
response.Should().HaveSetCookieHeader("fiscalos_refresh_cookie");
|
||||
await response.Should().BeJsonContentOfType<Refresh.Response>(HttpStatusCode.OK);
|
||||
|
||||
var oldRefreshTokenInDb = await ExecuteDbContextAsync(
|
||||
async context => await context.Set<RefreshToken>()
|
||||
.Include(t => t.User)
|
||||
.Where(t => t.UserId == user.Id && t.Token == refreshToken.Token && t.Revoked == true)
|
||||
.SingleOrDefaultAsync()
|
||||
);
|
||||
|
||||
oldRefreshTokenInDb.Should().NotBeNull();
|
||||
|
||||
var newRefreshTokenInDb = await ExecuteDbContextAsync(
|
||||
async context => await context.Set<RefreshToken>()
|
||||
.Include(t => t.User)
|
||||
.Where(t => t.UserId == user.Id && t.Revoked == false && t.Token != refreshToken.Token)
|
||||
.SingleOrDefaultAsync()
|
||||
);
|
||||
|
||||
newRefreshTokenInDb.Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
global using System.IdentityModel.Tokens.Jwt;
|
||||
global using System.Net;
|
||||
global using System.Net.Http.Headers;
|
||||
global using System.Net.Http.Json;
|
||||
global using System.Security.Claims;
|
||||
global using System.Security.Cryptography;
|
||||
|
||||
global using AwesomeAssertions.Execution;
|
||||
global using AwesomeAssertions.Primitives;
|
||||
global using FiscalOS.API.Tests.Assertions;
|
||||
global using FiscalOS.API.Tests.Infra;
|
||||
global using FiscalOS.Core.Authentication;
|
||||
global using FiscalOS.Core.Identity;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace FiscalOS.Infra.Tests.Assertions;
|
||||
|
||||
internal static class JwtTokenAssertionExtensions
|
||||
internal static class JwtTokenAssertions
|
||||
{
|
||||
public static AndConstraint<string> HaveClaim(
|
||||
this StringAssertions assertions,
|
||||
|
||||
Reference in New Issue
Block a user