Updeted Session Model and Session Opener Service

renamed RefreshToken in model to RefreshTokenHash - now we have a hash of token, not a token
This commit is contained in:
Artemy
2025-12-14 14:03:18 +07:00
parent 0ad6b52de5
commit cc2921d257
18 changed files with 1153 additions and 191 deletions
@@ -0,0 +1,51 @@
using AutoFixture;
using Govor.Application.Interfaces.Authentication;
using Govor.Application.Services.Authentication;
namespace Govor.Application.Tests.Services.Authentication;
[TestFixture]
public class JwtTokenHasherTests
{
private IJwtTokenHasher _jwtTokenHasher;
private Fixture _fixture;
[SetUp]
public void StarUp()
{
_fixture = new Fixture();
_jwtTokenHasher = new JwtTokenHasher();
}
[Test]
public void Given_Token_When_Hash_Then_Hash_And_Verify_Then_Result_Should_Be_True()
{
// Arrange
string token = _fixture.Create<string>();
// Act
string hash = _jwtTokenHasher.HashToken(token);
var result = _jwtTokenHasher.VerifyToken(token, hash);
// Assert
Assert.That(hash, Is.Not.EqualTo(token));
Assert.That(result, Is.True);
}
[Test]
public void Given_Token_NotToken_When_Hash_Should_Not_Be_True_Then_Result_Should_Be_False()
{
// Arrange
string token = _fixture.Create<string>();
string notToken = _fixture.Create<string>();
// Act
string hash = _jwtTokenHasher.HashToken(token);
var result = _jwtTokenHasher.VerifyToken(notToken, hash);
// Assert
Assert.That(result, Is.False);
}
}
@@ -6,6 +6,7 @@ using Microsoft.Extensions.Options;
using Moq;
using Govor.Application.Interfaces.Authentication;
using Govor.Application.Services.Authentication;
using Govor.Data.Repositories.Exceptions;
namespace Govor.Application.Tests.Services.UserSessions;
@@ -14,138 +15,141 @@ public class UserSessionOpenerTests
{
private Mock<IUserSessionsRepository> _repositoryMock;
private Mock<IJwtService> _jwtServiceMock;
private Mock<ILogger<UserSessionOpener>> _loggerMock;
private IOptions<JwtRefreshOption> _options;
private Mock<IJwtTokenHasher> _jwtTokenHasherMock;
private UserSessionOpener _service;
private User _user;
private Guid _sessionId;
private const string DeviceInfo = "Chrome on Windows";
private const string GeneratedToken = "new-refresh-token";
private const string NewAccessToken = "new-access-token";
private const string RefreshToken = "new-refresh-token";
private const string AccessToken = "new-access-token";
private const string TokenHash = "hashed-refresh-token";
[SetUp]
public void Setup()
{
_repositoryMock = new Mock<IUserSessionsRepository>();
_jwtServiceMock = new Mock<IJwtService>();
_loggerMock = new Mock<ILogger<UserSessionOpener>>();
_options = Options.Create(new JwtRefreshOption { RefreshTokenLifetimeDays = 30 });
_sessionId = Guid.NewGuid();
var loggerMock = new Mock<ILogger<UserSessionOpener>>();
_jwtTokenHasherMock = new Mock<IJwtTokenHasher>();
var options = Options.Create(new JwtRefreshOption { RefreshTokenLifetimeDays = 30 });
_user = new User
{
Id = Guid.NewGuid(),
Username = "test",
PasswordHash = "hashed",
Description = "some description",
PasswordHash = "hashed-password",
IconId = Guid.NewGuid(),
CreatedOn = DateOnly.FromDateTime(DateTime.UtcNow),
WasOnline = DateTime.UtcNow,
InviteId = Guid.NewGuid()
CreatedOn = DateOnly.FromDateTime(DateTime.Now),
WasOnline = DateTime.Now,
InviteId = Guid.NewGuid(),
// ...
};
_jwtServiceMock.Setup(j => j.GenerateRefreshTokenAsync(_user)).ReturnsAsync(RefreshToken);
_jwtTokenHasherMock.Setup(h => h.HashToken(RefreshToken)).Returns(TokenHash);
_jwtServiceMock.Setup(j => j.GenerateAccessTokenAsync(_user, It.IsAny<Guid>())).ReturnsAsync(AccessToken);
_jwtServiceMock.Setup(j => j.GenerateRefreshTokenAsync(_user)).ReturnsAsync(GeneratedToken);
_jwtServiceMock.Setup(j => j.GenerateAccessTokenAsync(_user, _sessionId)).ReturnsAsync(NewAccessToken);
_service = new UserSessionOpener(
_repositoryMock.Object,
_jwtServiceMock.Object,
_options,
_loggerMock.Object
_jwtTokenHasherMock.Object,
options,
loggerMock.Object
);
}
// UpdateExistingSessionAsync
[Test]
public async Task OpenSessionAsync_ShouldReturnExistingToken_IfSessionValid()
public async Task OpenSessionAsync_ShouldUpdateExistingSession_WhenFoundByDeviceInfo()
{
// Arrange
var session = new UserSession
var existingSessionId = Guid.NewGuid();
var existingSession = new UserSession
{
Id = _sessionId,
Id = existingSessionId,
UserId = _user.Id,
DeviceInfo = DeviceInfo,
RefreshToken = "valid-token",
RefreshTokenHash = "old-hash",
CreatedAt = DateTime.UtcNow.AddDays(-10),
ExpiresAt = DateTime.UtcNow.AddDays(10),
IsRevoked = false
IsRevoked = true
};
_repositoryMock.Setup(r => r.GetByUserIdAsync(_user.Id)).ReturnsAsync(new List<UserSession> { session });
// Act
var result = await _service.OpenSessionAsync(_user, DeviceInfo);
// Assert
Assert.That(result.refreshToken, Is.EqualTo(GeneratedToken));
Assert.That(result.accessToken, Is.EqualTo(NewAccessToken));
_repositoryMock.Verify(r => r.UpdateAsync(It.Is<UserSession>(s =>
s.RefreshToken == GeneratedToken &&
s.IsRevoked == false)), Times.Once);
}
[Test]
public async Task OpenSessionAsync_ShouldUpdateSession_IfExpiredOrRevoked()
{
// Arrange
var session = new UserSession
{
Id = _sessionId,
UserId = _user.Id,
DeviceInfo = DeviceInfo,
RefreshToken = "old-token",
CreatedAt = DateTime.UtcNow.AddDays(-40),
ExpiresAt = DateTime.UtcNow.AddDays(-1),
IsRevoked = false
};
_repositoryMock.Setup(r => r.GetByUserIdAsync(_user.Id)).ReturnsAsync(new List<UserSession> { session });
// Act
var result = await _service.OpenSessionAsync(_user, DeviceInfo);
// Assert
Assert.That(result.refreshToken, Is.EqualTo(GeneratedToken));
Assert.That(result.accessToken, Is.EqualTo(NewAccessToken));
_repositoryMock.Verify(r => r.UpdateAsync(It.Is<UserSession>(s =>
s.RefreshToken == GeneratedToken &&
s.IsRevoked == false)), Times.Once);
}
[Test]
public async Task OpenSessionAsync_ShouldCreateNewSession_IfNoneExists()
{
// Arrange
_repositoryMock.Setup(r => r.GetByUserIdAsync(_user.Id)).ReturnsAsync(new List<UserSession>());
_jwtServiceMock.Setup(j => j.GenerateAccessTokenAsync(_user, It.IsAny<Guid>())).ReturnsAsync(NewAccessToken);
_repositoryMock.Setup(r => r.GetByUserIdAsync(_user.Id)).ReturnsAsync(new List<UserSession> { existingSession });
// Act
var result = await _service.OpenSessionAsync(_user, DeviceInfo);
// Assert
Assert.That(result.refreshToken, Is.EqualTo(GeneratedToken));
Assert.That(result.accessToken, Is.EqualTo(NewAccessToken));
Assert.Multiple(() =>
{
Assert.That(result.refreshToken, Is.EqualTo(RefreshToken), "Должен быть возвращен новый RefreshToken.");
Assert.That(result.accessToken, Is.EqualTo(AccessToken), "Должен быть возвращен новый AccessToken.");
});
_repositoryMock.Verify(r => r.UpdateAsync(It.Is<UserSession>(s =>
s.Id == existingSessionId &&
s.RefreshTokenHash == TokenHash &&
s.IsRevoked == false &&
s.ExpiresAt > DateTime.UtcNow
)), Times.Once, "Должен быть вызван метод UpdateAsync с новыми данными.");
_repositoryMock.Verify(r => r.AddAsync(It.IsAny<UserSession>()), Times.Never);
}
// CreateNewSessionAsync
[Test]
public async Task OpenSessionAsync_ShouldCreateNewSession_IfNoneFound()
{
// Arrange
_repositoryMock.Setup(r => r.GetByUserIdAsync(_user.Id)).ReturnsAsync(new List<UserSession>());
// Act
var result = await _service.OpenSessionAsync(_user, DeviceInfo);
// Assert
Assert.Multiple(() =>
{
Assert.That(result.refreshToken, Is.EqualTo(RefreshToken), "Должен быть возвращен новый RefreshToken.");
Assert.That(result.accessToken, Is.EqualTo(AccessToken), "Должен быть возвращен новый AccessToken.");
});
_repositoryMock.Verify(r => r.AddAsync(It.Is<UserSession>(s =>
s.UserId == _user.Id &&
s.DeviceInfo == DeviceInfo &&
s.RefreshToken == GeneratedToken
)), Times.Once);
s.RefreshTokenHash == TokenHash &&
s.IsRevoked == false
)), Times.Once, "Должен быть вызван метод AddAsync для создания новой сессии.");
_repositoryMock.Verify(r => r.UpdateAsync(It.IsAny<UserSession>()), Times.Never);
}
[Test]
public async Task OpenSessionAsync_ShouldCreateNewSession_WhenNotFoundByKeyExceptionThrown()
public async Task OpenSessionAsync_ShouldCreateNewSession_WhenRepositoryThrowsNotFoundByKeyException()
{
// Arrange
_repositoryMock
.Setup(r => r.GetByUserIdAsync(_user.Id))
.ThrowsAsync(new Govor.Data.Repositories.Exceptions.NotFoundByKeyException<Guid>(_user.Id, "userId"));
_jwtServiceMock.Setup(j => j.GenerateAccessTokenAsync(_user, It.IsAny<Guid>())).ReturnsAsync(NewAccessToken);
.ThrowsAsync(new NotFoundByKeyException<Guid>(_user.Id, "userId"));
// Act
var result = await _service.OpenSessionAsync(_user, DeviceInfo);
// Assert
Assert.That(result.refreshToken, Is.EqualTo(GeneratedToken));
Assert.That(result.accessToken, Is.EqualTo(NewAccessToken));
_repositoryMock.Verify(r => r.AddAsync(It.IsAny<UserSession>()), Times.Once);
Assert.Multiple(() =>
{
Assert.That(result.refreshToken, Is.EqualTo(RefreshToken), "Должен быть возвращен новый RefreshToken.");
Assert.That(result.accessToken, Is.EqualTo(AccessToken), "Должен быть возвращен новый AccessToken.");
});
_repositoryMock.Verify(r => r.AddAsync(It.Is<UserSession>(s =>
s.RefreshTokenHash == TokenHash
)), Times.Once, "Должен быть вызван AddAsync после перехвата исключения.");
_repositoryMock.Verify(r => r.UpdateAsync(It.IsAny<UserSession>()), Times.Never);
}
}
@@ -58,7 +58,7 @@ public class UserSessionRefresherTests
_session = new UserSession
{
RefreshToken = OldRefreshToken,
RefreshTokenHash = OldRefreshToken,
UserId = _user.Id,
DeviceInfo = "Chrome",
CreatedAt = DateTime.UtcNow.AddDays(-5),
@@ -71,7 +71,7 @@ public class UserSessionRefresherTests
public async Task RefreshTokenAsync_ValidToken_ReturnsNewTokensAndCreatesNewSession()
{
// Arrange
_sessionsRepoMock.Setup(r => r.GetByRefreshTokenAsync(OldRefreshToken)).ReturnsAsync(_session);
_sessionsRepoMock.Setup(r => r.GetByHashedRefreshTokenAsync(OldRefreshToken)).ReturnsAsync(_session);
_usersRepoMock.Setup(r => r.FindByIdAsync(_user.Id)).ReturnsAsync(_user);
_jwtServiceMock.Setup(j => j.GenerateAccessTokenAsync(_user, _session.Id)).ReturnsAsync(NewAccessToken);
_jwtServiceMock.Setup(j => j.GenerateRefreshTokenAsync(_user)).ReturnsAsync(NewRefreshToken);
@@ -87,7 +87,7 @@ public class UserSessionRefresherTests
_sessionsRepoMock.Verify(r => r.UpdateAsync(_session), Times.Once);
_sessionsRepoMock.Verify(r => r.AddAsync(It.Is<UserSession>(s =>
s.UserId == _user.Id &&
s.RefreshToken == NewRefreshToken &&
s.RefreshTokenHash == NewRefreshToken &&
s.DeviceInfo == _session.DeviceInfo)), Times.Once);
}
@@ -96,7 +96,7 @@ public class UserSessionRefresherTests
{
// Arrange
_session.IsRevoked = true;
_sessionsRepoMock.Setup(r => r.GetByRefreshTokenAsync(OldRefreshToken)).ReturnsAsync(_session);
_sessionsRepoMock.Setup(r => r.GetByHashedRefreshTokenAsync(OldRefreshToken)).ReturnsAsync(_session);
// Act & Assert
var ex = Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>
@@ -110,7 +110,7 @@ public class UserSessionRefresherTests
{
// Arrange
_session.ExpiresAt = DateTime.UtcNow.AddMinutes(-1);
_sessionsRepoMock.Setup(r => r.GetByRefreshTokenAsync(OldRefreshToken)).ReturnsAsync(_session);
_sessionsRepoMock.Setup(r => r.GetByHashedRefreshTokenAsync(OldRefreshToken)).ReturnsAsync(_session);
// Act & Assert
var ex = Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>
@@ -123,7 +123,7 @@ public class UserSessionRefresherTests
public void RefreshTokenAsync_TokenNotFound_ThrowsUnauthorizedAccessException()
{
// Arrange
_sessionsRepoMock.Setup(r => r.GetByRefreshTokenAsync(OldRefreshToken))
_sessionsRepoMock.Setup(r => r.GetByHashedRefreshTokenAsync(OldRefreshToken))
.ThrowsAsync(new NotFoundByKeyException<string>("token", OldRefreshToken));
// Act & Assert
var ex = Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>