2026-02-13 21:20:05 -06:00
|
|
|
namespace FiscalOS.API.Tests.Integration.Auth;
|
2026-02-03 11:49:02 -06:00
|
|
|
|
|
|
|
|
public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
|
|
|
|
|
{
|
2026-02-13 21:20:05 -06:00
|
|
|
private static readonly Uri RefreshUri = new("/auth/refresh", UriKind.Relative);
|
2026-02-03 11:49:02 -06:00
|
|
|
|
|
|
|
|
[Fact]
|
2026-02-03 21:03:41 -06:00
|
|
|
public async Task Refresh_WhenCalledWithNoAccessToken_ItShouldReturn401WithProblemDetails()
|
2026-02-03 11:49:02 -06:00
|
|
|
{
|
2026-02-03 21:03:41 -06:00
|
|
|
var response = await Client.PostAsync(RefreshUri, null, TestContext.Current.CancellationToken);
|
2026-02-03 11:49:02 -06:00
|
|
|
|
2026-02-03 21:03:41 -06:00
|
|
|
await response.Should().BeProblemDetails(HttpStatusCode.Unauthorized);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Refresh_WhenCalledWithNonExistentRefreshToken_ItShouldReturn400WithProblemDetails()
|
|
|
|
|
{
|
2026-02-13 21:20:05 -06:00
|
|
|
using var request = HttpRequestBuilder.New()
|
|
|
|
|
.Post(RefreshUri)
|
|
|
|
|
.WithUserId(Guid.NewGuid())
|
|
|
|
|
.WithRefreshCookie("nonexistenttoken")
|
2026-02-03 21:03:41 -06:00
|
|
|
.Build();
|
|
|
|
|
|
|
|
|
|
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
|
|
|
|
|
|
|
|
|
await response.Should().BeProblemDetails(HttpStatusCode.BadRequest);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Refresh_WhenCalledWithTokenBelongingToDifferentUser_ItShouldReturn403WithProblemDetails()
|
|
|
|
|
{
|
2026-03-08 09:15:45 -05:00
|
|
|
var (users, refreshToken) = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
2026-02-03 21:03:41 -06:00
|
|
|
{
|
|
|
|
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
|
|
|
|
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
2026-02-06 06:48:34 -06:00
|
|
|
var encryptor = sp.GetRequiredService<IEncryptor>();
|
2026-02-03 21:03:41 -06:00
|
|
|
|
2026-02-12 20:47:40 -06:00
|
|
|
var user1EncryptionKey = await encryptor.GenerateEncryptedKeyAsync(ct);
|
|
|
|
|
var user2EncryptionKey = await encryptor.GenerateEncryptedKeyAsync(ct);
|
2026-02-06 06:48:34 -06:00
|
|
|
var user1 = User.From("User1", passwordHasher.Hash("@Password1"), user1EncryptionKey);
|
|
|
|
|
var user2 = User.From("User2", passwordHasher.Hash("@Password2"), user2EncryptionKey);
|
2026-02-03 21:03:41 -06:00
|
|
|
var refreshToken1 = tokenGenerator.GenerateRefreshToken(user2);
|
|
|
|
|
var refreshToken2 = tokenGenerator.GenerateRefreshToken(user2);
|
|
|
|
|
user2.AddRefreshToken(refreshToken1);
|
|
|
|
|
user2.AddRefreshToken(refreshToken2);
|
|
|
|
|
|
|
|
|
|
context.Add(user1);
|
|
|
|
|
context.Add(user2);
|
|
|
|
|
|
2026-02-12 20:47:40 -06:00
|
|
|
await context.SaveChangesAsync(ct);
|
2026-02-03 21:03:41 -06:00
|
|
|
return (new User[] { user1, user2 }, refreshToken1);
|
2026-02-12 20:47:40 -06:00
|
|
|
}, TestContext.Current.CancellationToken);
|
2026-02-03 21:03:41 -06:00
|
|
|
|
2026-02-13 21:20:05 -06:00
|
|
|
using var request = HttpRequestBuilder.New()
|
|
|
|
|
.Post(RefreshUri)
|
|
|
|
|
.WithUserId(users[0].Id)
|
|
|
|
|
.WithRefreshCookie(refreshToken.Token)
|
2026-02-03 21:03:41 -06:00
|
|
|
.Build();
|
|
|
|
|
|
|
|
|
|
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
|
|
|
|
|
|
|
|
|
await response.Should().BeProblemDetails(HttpStatusCode.Forbidden);
|
|
|
|
|
|
2026-03-08 09:15:45 -05:00
|
|
|
var unrevokedTokensCountForUser2 = await Api.ExecuteAsync(
|
2026-02-12 20:47:40 -06:00
|
|
|
async (context, ct) => await context.Set<RefreshToken>()
|
2026-02-03 21:03:41 -06:00
|
|
|
.Include(t => t.User)
|
|
|
|
|
.Where(t => t.UserId == users[1].Id && t.Revoked == false)
|
2026-02-12 20:47:40 -06:00
|
|
|
.CountAsync(ct),
|
|
|
|
|
TestContext.Current.CancellationToken
|
2026-02-03 21:03:41 -06:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
unrevokedTokensCountForUser2.Should().Be(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Refresh_WhenCalledWithRevokedRefreshToken_ItShouldReturn400WithProblemDetails()
|
|
|
|
|
{
|
2026-03-08 09:15:45 -05:00
|
|
|
var (user, refreshToken) = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
2026-02-03 21:03:41 -06:00
|
|
|
{
|
|
|
|
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
|
|
|
|
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
2026-02-06 06:48:34 -06:00
|
|
|
var encryptor = sp.GetRequiredService<IEncryptor>();
|
2026-02-03 21:03:41 -06:00
|
|
|
|
2026-02-12 20:47:40 -06:00
|
|
|
var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(ct);
|
2026-02-06 06:48:34 -06:00
|
|
|
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
|
2026-02-03 21:03:41 -06:00
|
|
|
var refreshToken = tokenGenerator.GenerateRefreshToken(user);
|
|
|
|
|
refreshToken.Revoke();
|
|
|
|
|
|
|
|
|
|
user.AddRefreshToken(refreshToken);
|
|
|
|
|
|
|
|
|
|
context.Add(user);
|
|
|
|
|
|
2026-02-12 20:47:40 -06:00
|
|
|
await context.SaveChangesAsync(ct);
|
2026-02-03 21:03:41 -06:00
|
|
|
return (user, refreshToken);
|
2026-02-12 20:47:40 -06:00
|
|
|
}, TestContext.Current.CancellationToken);
|
2026-02-03 21:03:41 -06:00
|
|
|
|
2026-02-13 21:20:05 -06:00
|
|
|
using var request = HttpRequestBuilder.New()
|
|
|
|
|
.Post(RefreshUri)
|
|
|
|
|
.WithUserId(user.Id)
|
|
|
|
|
.WithRefreshCookie(refreshToken.Token)
|
2026-02-03 21:03:41 -06:00
|
|
|
.Build();
|
|
|
|
|
|
|
|
|
|
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
|
|
|
|
|
|
|
|
|
await response.Should().BeProblemDetails(HttpStatusCode.BadRequest);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Refresh_WhenCalledWithExpiredRefreshToken_ItShouldReturn400WithProblemDetails()
|
|
|
|
|
{
|
2026-03-08 09:15:45 -05:00
|
|
|
var (user, refreshToken) = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
2026-02-03 21:03:41 -06:00
|
|
|
{
|
|
|
|
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
|
|
|
|
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
|
|
|
|
var timeProvider = sp.GetRequiredService<TimeProvider>();
|
2026-02-06 06:48:34 -06:00
|
|
|
var encryptor = sp.GetRequiredService<IEncryptor>();
|
2026-02-03 21:03:41 -06:00
|
|
|
|
2026-02-12 20:47:40 -06:00
|
|
|
var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(ct);
|
2026-02-06 06:48:34 -06:00
|
|
|
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
|
2026-02-03 21:03:41 -06:00
|
|
|
var refreshToken = RefreshToken.From(user.Id, "expiredtoken", timeProvider.GetUtcNow().AddHours(-1));
|
|
|
|
|
user.AddRefreshToken(refreshToken);
|
|
|
|
|
|
|
|
|
|
context.Add(user);
|
|
|
|
|
|
2026-02-12 20:47:40 -06:00
|
|
|
await context.SaveChangesAsync(ct);
|
2026-02-03 21:03:41 -06:00
|
|
|
return (user, refreshToken);
|
2026-02-12 20:47:40 -06:00
|
|
|
}, TestContext.Current.CancellationToken);
|
2026-02-03 21:03:41 -06:00
|
|
|
|
2026-02-13 21:20:05 -06:00
|
|
|
using var request = HttpRequestBuilder.New()
|
|
|
|
|
.Post(RefreshUri)
|
|
|
|
|
.WithUserId(user.Id)
|
|
|
|
|
.WithRefreshCookie(refreshToken.Token)
|
2026-02-03 21:03:41 -06:00
|
|
|
.Build();
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
{
|
2026-03-08 09:15:45 -05:00
|
|
|
var (user, refreshToken) = await Api.ExecuteAsync(static async (context, ct, sp) =>
|
2026-02-03 21:03:41 -06:00
|
|
|
{
|
|
|
|
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
|
|
|
|
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
|
|
|
|
var timeProvider = sp.GetRequiredService<TimeProvider>();
|
2026-02-06 06:48:34 -06:00
|
|
|
var encryptor = sp.GetRequiredService<IEncryptor>();
|
2026-02-03 21:03:41 -06:00
|
|
|
|
2026-02-12 20:47:40 -06:00
|
|
|
var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(ct);
|
2026-02-06 06:48:34 -06:00
|
|
|
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
|
2026-02-03 21:03:41 -06:00
|
|
|
var refreshToken = tokenGenerator.GenerateRefreshToken(user);
|
|
|
|
|
user.AddRefreshToken(refreshToken);
|
|
|
|
|
|
|
|
|
|
context.Add(user);
|
|
|
|
|
|
2026-02-12 20:47:40 -06:00
|
|
|
await context.SaveChangesAsync(ct);
|
2026-02-03 21:03:41 -06:00
|
|
|
return (user, refreshToken);
|
2026-02-12 20:47:40 -06:00
|
|
|
}, TestContext.Current.CancellationToken);
|
2026-02-03 21:03:41 -06:00
|
|
|
|
2026-02-13 21:20:05 -06:00
|
|
|
using var request = HttpRequestBuilder.New()
|
|
|
|
|
.Post(RefreshUri)
|
|
|
|
|
.WithBearerToken(JwtTokenBuilder.New()
|
|
|
|
|
.WithClaim(JwtRegisteredClaimNames.Sub, user.Id.ToString())
|
|
|
|
|
.WithExpiresAt(DateTime.UtcNow.AddMinutes(accessTokenExpiresAtOffset))
|
|
|
|
|
.Build())
|
|
|
|
|
.WithRefreshCookie(refreshToken.Token)
|
2026-02-03 21:03:41 -06:00
|
|
|
.Build();
|
|
|
|
|
|
|
|
|
|
var response = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
|
|
|
|
|
|
|
|
|
response.Should().HaveSetCookieHeader("fiscalos_refresh_cookie");
|
2026-02-13 21:20:05 -06:00
|
|
|
await response.Should().BeJsonContentOfType<API.Auth.Refresh.Response>(HttpStatusCode.OK);
|
2026-02-03 21:03:41 -06:00
|
|
|
|
2026-03-08 09:15:45 -05:00
|
|
|
var oldRefreshTokenInDb = await Api.ExecuteAsync(
|
2026-02-12 20:47:40 -06:00
|
|
|
async (context, ct) => await context.Set<RefreshToken>()
|
2026-02-03 21:03:41 -06:00
|
|
|
.Include(t => t.User)
|
|
|
|
|
.Where(t => t.UserId == user.Id && t.Token == refreshToken.Token && t.Revoked == true)
|
2026-02-12 20:47:40 -06:00
|
|
|
.SingleOrDefaultAsync(ct),
|
|
|
|
|
TestContext.Current.CancellationToken
|
2026-02-03 21:03:41 -06:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
oldRefreshTokenInDb.Should().NotBeNull();
|
|
|
|
|
|
2026-03-08 09:15:45 -05:00
|
|
|
var newRefreshTokenInDb = await Api.ExecuteAsync(
|
2026-02-12 20:47:40 -06:00
|
|
|
async (context, ct) => await context.Set<RefreshToken>()
|
2026-02-03 21:03:41 -06:00
|
|
|
.Include(t => t.User)
|
|
|
|
|
.Where(t => t.UserId == user.Id && t.Revoked == false && t.Token != refreshToken.Token)
|
2026-02-12 20:47:40 -06:00
|
|
|
.SingleOrDefaultAsync(ct),
|
|
|
|
|
TestContext.Current.CancellationToken
|
2026-02-03 21:03:41 -06:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
newRefreshTokenInDb.Should().NotBeNull();
|
2026-02-03 11:49:02 -06:00
|
|
|
}
|
|
|
|
|
}
|