Add test
Some checks failed
Code Coverage / Generate Coverage Report (push) Has been cancelled
Tests / Run Tests (9.0.x) (push) Has been cancelled
Tests / Docker Build Test (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled

This commit is contained in:
Yaojia Wang
2025-11-04 00:20:42 +01:00
parent 26be84de2c
commit 172d0de1fe
13 changed files with 2901 additions and 15 deletions

View File

@@ -0,0 +1,323 @@
using ColaFlow.Modules.Identity.Domain.Aggregates.Invitations;
using ColaFlow.Modules.Identity.Domain.Aggregates.Invitations.Events;
using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
using ColaFlow.Modules.Identity.Domain.Aggregates.Users;
using FluentAssertions;
using Xunit;
namespace ColaFlow.Modules.Identity.Domain.Tests.Aggregates;
public sealed class InvitationTests
{
private readonly TenantId _tenantId = TenantId.CreateUnique();
private readonly UserId _invitedBy = UserId.CreateUnique();
private const string TestEmail = "invite@example.com";
private const string TestTokenHash = "hashed_token_value";
[Fact]
public void Create_WithValidData_ShouldSucceed()
{
// Arrange & Act
var invitation = Invitation.Create(
_tenantId,
TestEmail,
TenantRole.TenantMember,
TestTokenHash,
_invitedBy);
// Assert
invitation.Should().NotBeNull();
invitation.Id.Should().NotBeNull();
invitation.TenantId.Should().Be(_tenantId);
invitation.Email.Should().Be(TestEmail.ToLowerInvariant());
invitation.Role.Should().Be(TenantRole.TenantMember);
invitation.TokenHash.Should().Be(TestTokenHash);
invitation.InvitedBy.Should().Be(_invitedBy);
invitation.ExpiresAt.Should().BeCloseTo(DateTime.UtcNow.AddDays(7), TimeSpan.FromSeconds(1));
invitation.AcceptedAt.Should().BeNull();
invitation.IsPending.Should().BeTrue();
invitation.IsExpired.Should().BeFalse();
invitation.IsAccepted.Should().BeFalse();
}
[Fact]
public void Create_WithTenantOwnerRole_ShouldThrowException()
{
// Arrange & Act
var act = () => Invitation.Create(
_tenantId,
TestEmail,
TenantRole.TenantOwner,
TestTokenHash,
_invitedBy);
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Cannot invite users with role TenantOwner*");
}
[Fact]
public void Create_WithAIAgentRole_ShouldThrowException()
{
// Arrange & Act
var act = () => Invitation.Create(
_tenantId,
TestEmail,
TenantRole.AIAgent,
TestTokenHash,
_invitedBy);
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Cannot invite users with role AIAgent*");
}
[Fact]
public void Create_WithEmptyEmail_ShouldThrowException()
{
// Arrange & Act
var act = () => Invitation.Create(
_tenantId,
string.Empty,
TenantRole.TenantMember,
TestTokenHash,
_invitedBy);
// Assert
act.Should().Throw<ArgumentException>()
.WithMessage("*Email cannot be empty*");
}
[Fact]
public void Create_WithEmptyTokenHash_ShouldThrowException()
{
// Arrange & Act
var act = () => Invitation.Create(
_tenantId,
TestEmail,
TenantRole.TenantMember,
string.Empty,
_invitedBy);
// Assert
act.Should().Throw<ArgumentException>()
.WithMessage("*Token hash cannot be empty*");
}
[Fact]
public void Create_ShouldRaiseUserInvitedEvent()
{
// Arrange & Act
var invitation = Invitation.Create(
_tenantId,
TestEmail,
TenantRole.TenantAdmin,
TestTokenHash,
_invitedBy);
// Assert
invitation.DomainEvents.Should().ContainSingle();
invitation.DomainEvents.Should().ContainItemsAssignableTo<UserInvitedEvent>();
var domainEvent = invitation.DomainEvents.First() as UserInvitedEvent;
domainEvent.Should().NotBeNull();
domainEvent!.InvitationId.Should().Be(invitation.Id);
domainEvent.TenantId.Should().Be(_tenantId);
domainEvent.Email.Should().Be(TestEmail.ToLowerInvariant());
domainEvent.Role.Should().Be(TenantRole.TenantAdmin);
domainEvent.InvitedBy.Should().Be(_invitedBy);
}
[Fact]
public void Accept_WhenPending_ShouldMarkAccepted()
{
// Arrange
var invitation = Invitation.Create(
_tenantId,
TestEmail,
TenantRole.TenantMember,
TestTokenHash,
_invitedBy);
invitation.ClearDomainEvents();
// Act
invitation.Accept();
// Assert
invitation.IsAccepted.Should().BeTrue();
invitation.IsPending.Should().BeFalse();
invitation.AcceptedAt.Should().NotBeNull();
invitation.AcceptedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
invitation.DomainEvents.Should().ContainSingle();
invitation.DomainEvents.Should().ContainItemsAssignableTo<InvitationAcceptedEvent>();
}
[Fact]
public void Accept_WhenExpired_ShouldThrowException()
{
// Arrange
var invitation = Invitation.Create(
_tenantId,
TestEmail,
TenantRole.TenantMember,
TestTokenHash,
_invitedBy);
// Force expiration by canceling
invitation.Cancel();
// Act
var act = () => invitation.Accept();
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Invitation has expired*");
}
[Fact]
public void Accept_WhenAlreadyAccepted_ShouldThrowException()
{
// Arrange
var invitation = Invitation.Create(
_tenantId,
TestEmail,
TenantRole.TenantMember,
TestTokenHash,
_invitedBy);
invitation.Accept();
// Act
var act = () => invitation.Accept();
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Invitation has already been accepted*");
}
[Fact]
public void Cancel_WhenPending_ShouldMarkCancelled()
{
// Arrange
var invitation = Invitation.Create(
_tenantId,
TestEmail,
TenantRole.TenantMember,
TestTokenHash,
_invitedBy);
invitation.ClearDomainEvents();
// Act
invitation.Cancel();
// Assert
invitation.IsExpired.Should().BeTrue();
invitation.IsPending.Should().BeFalse();
invitation.ExpiresAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
invitation.DomainEvents.Should().ContainSingle();
invitation.DomainEvents.Should().ContainItemsAssignableTo<InvitationCancelledEvent>();
}
[Fact]
public void Cancel_WhenAccepted_ShouldThrowException()
{
// Arrange
var invitation = Invitation.Create(
_tenantId,
TestEmail,
TenantRole.TenantMember,
TestTokenHash,
_invitedBy);
invitation.Accept();
// Act
var act = () => invitation.Cancel();
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Cannot cancel non-pending invitation*");
}
[Fact]
public void IsPending_WithPendingInvitation_ShouldReturnTrue()
{
// Arrange
var invitation = Invitation.Create(
_tenantId,
TestEmail,
TenantRole.TenantMember,
TestTokenHash,
_invitedBy);
// Act & Assert
invitation.IsPending.Should().BeTrue();
invitation.IsExpired.Should().BeFalse();
invitation.IsAccepted.Should().BeFalse();
}
[Fact]
public void IsPending_WithAcceptedInvitation_ShouldReturnFalse()
{
// Arrange
var invitation = Invitation.Create(
_tenantId,
TestEmail,
TenantRole.TenantMember,
TestTokenHash,
_invitedBy);
invitation.Accept();
// Act & Assert
invitation.IsPending.Should().BeFalse();
invitation.IsAccepted.Should().BeTrue();
}
[Fact]
public void ValidateForAcceptance_WithValidInvitation_ShouldNotThrow()
{
// Arrange
var invitation = Invitation.Create(
_tenantId,
TestEmail,
TenantRole.TenantMember,
TestTokenHash,
_invitedBy);
// Act & Assert
var act = () => invitation.ValidateForAcceptance();
act.Should().NotThrow();
}
[Fact]
public void ValidateForAcceptance_WithExpiredInvitation_ShouldThrow()
{
// Arrange
var invitation = Invitation.Create(
_tenantId,
TestEmail,
TenantRole.TenantMember,
TestTokenHash,
_invitedBy);
invitation.Cancel(); // Force expiration
// Act & Assert
var act = () => invitation.ValidateForAcceptance();
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Invitation has expired*");
}
[Fact]
public void ValidateForAcceptance_WithAcceptedInvitation_ShouldThrow()
{
// Arrange
var invitation = Invitation.Create(
_tenantId,
TestEmail,
TenantRole.TenantMember,
TestTokenHash,
_invitedBy);
invitation.Accept();
// Act & Assert
var act = () => invitation.ValidateForAcceptance();
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Invitation has already been accepted*");
}
}

View File

@@ -302,4 +302,225 @@ public sealed class UserTests
act.Should().Throw<InvalidOperationException>()
.WithMessage("Cannot update SSO profile for local users");
}
[Fact]
public void VerifyEmail_WhenUnverified_ShouldSetVerifiedAt()
{
// Arrange
var user = User.CreateLocal(
_tenantId,
Email.Create("test@example.com"),
"hash",
FullName.Create("John Doe"));
user.ClearDomainEvents();
// Act
user.VerifyEmail();
// Assert
user.IsEmailVerified.Should().BeTrue();
user.EmailVerifiedAt.Should().NotBeNull();
user.EmailVerificationToken.Should().BeNull();
user.DomainEvents.Should().ContainSingle();
user.DomainEvents.Should().ContainItemsAssignableTo<EmailVerifiedEvent>();
}
[Fact]
public void VerifyEmail_WhenAlreadyVerified_ShouldBeIdempotent()
{
// Arrange
var user = User.CreateLocal(
_tenantId,
Email.Create("test@example.com"),
"hash",
FullName.Create("John Doe"));
user.VerifyEmail();
var firstVerifiedAt = user.EmailVerifiedAt;
user.ClearDomainEvents();
// Act
user.VerifyEmail();
// Assert
user.EmailVerifiedAt.Should().Be(firstVerifiedAt);
user.DomainEvents.Should().BeEmpty(); // No new event for idempotent operation
}
[Fact]
public void SetEmailVerificationToken_ShouldHashToken()
{
// Arrange
var user = User.CreateLocal(
_tenantId,
Email.Create("test@example.com"),
"hash",
FullName.Create("John Doe"));
const string token = "verification_token";
// Act
user.SetEmailVerificationToken(token);
// Assert
user.EmailVerificationToken.Should().Be(token);
}
[Fact]
public void SetPasswordResetToken_ShouldSetTokenAndExpiration()
{
// Arrange
var user = User.CreateLocal(
_tenantId,
Email.Create("test@example.com"),
"hash",
FullName.Create("John Doe"));
const string token = "reset_token";
var expiresAt = DateTime.UtcNow.AddHours(1);
// Act
user.SetPasswordResetToken(token, expiresAt);
// Assert
user.PasswordResetToken.Should().Be(token);
user.PasswordResetTokenExpiresAt.Should().Be(expiresAt);
}
[Fact]
public void SetPasswordResetToken_ForSsoUser_ShouldThrowException()
{
// Arrange
var user = User.CreateFromSso(
_tenantId,
AuthenticationProvider.Google,
"google-123",
Email.Create("test@example.com"),
FullName.Create("John Doe"));
// Act
var act = () => user.SetPasswordResetToken("token", DateTime.UtcNow.AddHours(1));
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Cannot set password reset token for SSO users*");
}
[Fact]
public void ClearPasswordResetToken_ShouldClearTokenAndExpiration()
{
// Arrange
var user = User.CreateLocal(
_tenantId,
Email.Create("test@example.com"),
"hash",
FullName.Create("John Doe"));
user.SetPasswordResetToken("token", DateTime.UtcNow.AddHours(1));
// Act
user.ClearPasswordResetToken();
// Assert
user.PasswordResetToken.Should().BeNull();
user.PasswordResetTokenExpiresAt.Should().BeNull();
}
[Fact]
public void RecordLoginWithEvent_ShouldUpdateLastLogin()
{
// Arrange
var user = User.CreateLocal(
_tenantId,
Email.Create("test@example.com"),
"hash",
FullName.Create("John Doe"));
user.ClearDomainEvents();
// Act
user.RecordLoginWithEvent(_tenantId, "192.168.1.1", "Mozilla/5.0");
// Assert
user.LastLoginAt.Should().NotBeNull();
user.LastLoginAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
}
[Fact]
public void RecordLoginWithEvent_ShouldRaiseDomainEvent()
{
// Arrange
var user = User.CreateLocal(
_tenantId,
Email.Create("test@example.com"),
"hash",
FullName.Create("John Doe"));
user.ClearDomainEvents();
const string ipAddress = "192.168.1.1";
const string userAgent = "Mozilla/5.0";
// Act
user.RecordLoginWithEvent(_tenantId, ipAddress, userAgent);
// Assert
user.DomainEvents.Should().ContainSingle();
user.DomainEvents.Should().ContainItemsAssignableTo<UserLoggedInEvent>();
var domainEvent = user.DomainEvents.First() as UserLoggedInEvent;
domainEvent.Should().NotBeNull();
domainEvent!.UserId.Should().Be(user.Id);
domainEvent.TenantId.Should().Be(_tenantId);
domainEvent.IpAddress.Should().Be(ipAddress);
domainEvent.UserAgent.Should().Be(userAgent);
}
[Fact]
public void Delete_ShouldChangeStatusToDeleted()
{
// Arrange
var user = User.CreateLocal(
_tenantId,
Email.Create("test@example.com"),
"hash",
FullName.Create("John Doe"));
// Act
user.Delete();
// Assert
user.Status.Should().Be(UserStatus.Deleted);
user.UpdatedAt.Should().NotBeNull();
}
[Fact]
public void Suspend_WithDeletedUser_ShouldThrowException()
{
// Arrange
var user = User.CreateLocal(
_tenantId,
Email.Create("test@example.com"),
"hash",
FullName.Create("John Doe"));
user.Delete();
// Act
var act = () => user.Suspend("Test reason");
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Cannot suspend deleted user*");
}
[Fact]
public void Reactivate_WithDeletedUser_ShouldThrowException()
{
// Arrange
var user = User.CreateLocal(
_tenantId,
Email.Create("test@example.com"),
"hash",
FullName.Create("John Doe"));
user.Delete();
// Act
var act = () => user.Reactivate();
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Cannot reactivate deleted user*");
}
}

View File

@@ -0,0 +1,176 @@
using ColaFlow.Modules.Identity.Domain.Entities;
using FluentAssertions;
using Xunit;
namespace ColaFlow.Modules.Identity.Domain.Tests.Entities;
public sealed class EmailRateLimitTests
{
private readonly Guid _tenantId = Guid.NewGuid();
private const string TestEmail = "user@example.com";
private const string OperationType = "verification";
[Fact]
public void Create_WithValidData_ShouldSucceed()
{
// Arrange & Act
var rateLimit = EmailRateLimit.Create(TestEmail, _tenantId, OperationType);
// Assert
rateLimit.Should().NotBeNull();
rateLimit.Id.Should().NotBe(Guid.Empty);
rateLimit.Email.Should().Be(TestEmail.ToLower());
rateLimit.TenantId.Should().Be(_tenantId);
rateLimit.OperationType.Should().Be(OperationType);
rateLimit.AttemptsCount.Should().Be(1);
rateLimit.LastSentAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
}
[Fact]
public void Create_ShouldNormalizeEmail()
{
// Arrange
const string mixedCaseEmail = "User@EXAMPLE.COM";
// Act
var rateLimit = EmailRateLimit.Create(mixedCaseEmail, _tenantId, OperationType);
// Assert
rateLimit.Email.Should().Be("user@example.com");
}
[Fact]
public void Create_WithEmptyEmail_ShouldThrowException()
{
// Arrange & Act
var act = () => EmailRateLimit.Create(string.Empty, _tenantId, OperationType);
// Assert
act.Should().Throw<ArgumentException>()
.WithMessage("*Email cannot be empty*");
}
[Fact]
public void Create_WithEmptyOperationType_ShouldThrowException()
{
// Arrange & Act
var act = () => EmailRateLimit.Create(TestEmail, _tenantId, string.Empty);
// Assert
act.Should().Throw<ArgumentException>()
.WithMessage("*Operation type cannot be empty*");
}
[Fact]
public void RecordAttempt_ShouldIncrementCount()
{
// Arrange
var rateLimit = EmailRateLimit.Create(TestEmail, _tenantId, OperationType);
var initialCount = rateLimit.AttemptsCount;
var initialLastSentAt = rateLimit.LastSentAt;
// Wait a bit to ensure time difference
System.Threading.Thread.Sleep(10);
// Act
rateLimit.RecordAttempt();
// Assert
rateLimit.AttemptsCount.Should().Be(initialCount + 1);
rateLimit.LastSentAt.Should().BeAfter(initialLastSentAt);
}
[Fact]
public void RecordAttempt_MultipleCallsMultiple_ShouldIncrementCorrectly()
{
// Arrange
var rateLimit = EmailRateLimit.Create(TestEmail, _tenantId, OperationType);
// Act
rateLimit.RecordAttempt();
rateLimit.RecordAttempt();
rateLimit.RecordAttempt();
// Assert
rateLimit.AttemptsCount.Should().Be(4); // 1 initial + 3 increments
}
[Fact]
public void ResetAttempts_ShouldResetCountToOne()
{
// Arrange
var rateLimit = EmailRateLimit.Create(TestEmail, _tenantId, OperationType);
rateLimit.RecordAttempt();
rateLimit.RecordAttempt();
rateLimit.RecordAttempt();
// Act
rateLimit.ResetAttempts();
// Assert
rateLimit.AttemptsCount.Should().Be(1);
}
[Fact]
public void ResetAttempts_ShouldUpdateLastSentAt()
{
// Arrange
var rateLimit = EmailRateLimit.Create(TestEmail, _tenantId, OperationType);
var initialLastSentAt = rateLimit.LastSentAt;
// Wait a bit to ensure time difference
System.Threading.Thread.Sleep(10);
// Act
rateLimit.ResetAttempts();
// Assert
rateLimit.LastSentAt.Should().BeAfter(initialLastSentAt);
rateLimit.LastSentAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
}
[Fact]
public void IsWindowExpired_WithinWindow_ShouldReturnFalse()
{
// Arrange
var rateLimit = EmailRateLimit.Create(TestEmail, _tenantId, OperationType);
var window = TimeSpan.FromMinutes(5);
// Act
var isExpired = rateLimit.IsWindowExpired(window);
// Assert
isExpired.Should().BeFalse();
}
[Fact]
public void IsWindowExpired_OutsideWindow_ShouldReturnTrue()
{
// Arrange
var rateLimit = EmailRateLimit.Create(TestEmail, _tenantId, OperationType);
var window = TimeSpan.FromMilliseconds(1);
// Wait for window to expire
System.Threading.Thread.Sleep(10);
// Act
var isExpired = rateLimit.IsWindowExpired(window);
// Assert
isExpired.Should().BeTrue();
}
[Fact]
public void IsWindowExpired_WithZeroWindow_ShouldReturnTrue()
{
// Arrange
var rateLimit = EmailRateLimit.Create(TestEmail, _tenantId, OperationType);
var window = TimeSpan.Zero;
// Act
var isExpired = rateLimit.IsWindowExpired(window);
// Assert
isExpired.Should().BeTrue();
}
}

View File

@@ -0,0 +1,161 @@
using ColaFlow.Modules.Identity.Domain.Aggregates.Users;
using ColaFlow.Modules.Identity.Domain.Entities;
using FluentAssertions;
using Xunit;
namespace ColaFlow.Modules.Identity.Domain.Tests.Entities;
public sealed class EmailVerificationTokenTests
{
private readonly UserId _userId = UserId.CreateUnique();
private const string TestTokenHash = "hashed_token_value";
[Fact]
public void Create_WithValidData_ShouldSucceed()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(24);
// Act
var token = EmailVerificationToken.Create(_userId, TestTokenHash, expiresAt);
// Assert
token.Should().NotBeNull();
token.Id.Should().NotBe(Guid.Empty);
token.UserId.Should().Be(_userId);
token.TokenHash.Should().Be(TestTokenHash);
token.ExpiresAt.Should().Be(expiresAt);
token.VerifiedAt.Should().BeNull();
token.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
token.IsExpired.Should().BeFalse();
token.IsVerified.Should().BeFalse();
token.IsValid.Should().BeTrue();
}
[Fact]
public void IsExpired_WithFutureExpiration_ShouldReturnFalse()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(24);
var token = EmailVerificationToken.Create(_userId, TestTokenHash, expiresAt);
// Act & Assert
token.IsExpired.Should().BeFalse();
}
[Fact]
public void IsExpired_WithPastExpiration_ShouldReturnTrue()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(-1);
var token = EmailVerificationToken.Create(_userId, TestTokenHash, expiresAt);
// Act & Assert
token.IsExpired.Should().BeTrue();
}
[Fact]
public void IsVerified_WhenNotVerified_ShouldReturnFalse()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(24);
var token = EmailVerificationToken.Create(_userId, TestTokenHash, expiresAt);
// Act & Assert
token.IsVerified.Should().BeFalse();
}
[Fact]
public void IsVerified_WhenVerified_ShouldReturnTrue()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(24);
var token = EmailVerificationToken.Create(_userId, TestTokenHash, expiresAt);
token.MarkAsVerified();
// Act & Assert
token.IsVerified.Should().BeTrue();
}
[Fact]
public void IsValid_WithValidToken_ShouldReturnTrue()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(24);
var token = EmailVerificationToken.Create(_userId, TestTokenHash, expiresAt);
// Act & Assert
token.IsValid.Should().BeTrue();
}
[Fact]
public void IsValid_WithExpiredToken_ShouldReturnFalse()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(-1);
var token = EmailVerificationToken.Create(_userId, TestTokenHash, expiresAt);
// Act & Assert
token.IsValid.Should().BeFalse();
}
[Fact]
public void IsValid_WithVerifiedToken_ShouldReturnFalse()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(24);
var token = EmailVerificationToken.Create(_userId, TestTokenHash, expiresAt);
token.MarkAsVerified();
// Act & Assert
token.IsValid.Should().BeFalse();
}
[Fact]
public void MarkAsVerified_WithValidToken_ShouldSetVerifiedAt()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(24);
var token = EmailVerificationToken.Create(_userId, TestTokenHash, expiresAt);
// Act
token.MarkAsVerified();
// Assert
token.VerifiedAt.Should().NotBeNull();
token.VerifiedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
token.IsVerified.Should().BeTrue();
token.IsValid.Should().BeFalse();
}
[Fact]
public void MarkAsVerified_WithExpiredToken_ShouldThrowException()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(-1);
var token = EmailVerificationToken.Create(_userId, TestTokenHash, expiresAt);
// Act
var act = () => token.MarkAsVerified();
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Token is not valid for verification*");
}
[Fact]
public void MarkAsVerified_WithAlreadyVerifiedToken_ShouldThrowException()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(24);
var token = EmailVerificationToken.Create(_userId, TestTokenHash, expiresAt);
token.MarkAsVerified();
// Act
var act = () => token.MarkAsVerified();
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Token is not valid for verification*");
}
}

View File

@@ -0,0 +1,214 @@
using ColaFlow.Modules.Identity.Domain.Aggregates.Users;
using ColaFlow.Modules.Identity.Domain.Entities;
using FluentAssertions;
using Xunit;
namespace ColaFlow.Modules.Identity.Domain.Tests.Entities;
public sealed class PasswordResetTokenTests
{
private readonly UserId _userId = UserId.CreateUnique();
private const string TestTokenHash = "hashed_token_value";
private const string TestIpAddress = "192.168.1.1";
private const string TestUserAgent = "Mozilla/5.0";
[Fact]
public void Create_WithValidData_ShouldSucceed()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(1);
// Act
var token = PasswordResetToken.Create(
_userId,
TestTokenHash,
expiresAt,
TestIpAddress,
TestUserAgent);
// Assert
token.Should().NotBeNull();
token.Id.Should().NotBe(Guid.Empty);
token.UserId.Should().Be(_userId);
token.TokenHash.Should().Be(TestTokenHash);
token.ExpiresAt.Should().Be(expiresAt);
token.IpAddress.Should().Be(TestIpAddress);
token.UserAgent.Should().Be(TestUserAgent);
token.UsedAt.Should().BeNull();
token.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
token.IsExpired.Should().BeFalse();
token.IsUsed.Should().BeFalse();
token.IsValid.Should().BeTrue();
}
[Fact]
public void Create_WithoutOptionalParameters_ShouldSucceed()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(1);
// Act
var token = PasswordResetToken.Create(_userId, TestTokenHash, expiresAt);
// Assert
token.Should().NotBeNull();
token.IpAddress.Should().BeNull();
token.UserAgent.Should().BeNull();
}
[Fact]
public void IsExpired_WithFutureExpiration_ShouldReturnFalse()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(1);
var token = PasswordResetToken.Create(_userId, TestTokenHash, expiresAt);
// Act & Assert
token.IsExpired.Should().BeFalse();
}
[Fact]
public void IsExpired_WithPastExpiration_ShouldReturnTrue()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(-1);
var token = PasswordResetToken.Create(_userId, TestTokenHash, expiresAt);
// Act & Assert
token.IsExpired.Should().BeTrue();
}
[Fact]
public void IsUsed_WhenNotUsed_ShouldReturnFalse()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(1);
var token = PasswordResetToken.Create(_userId, TestTokenHash, expiresAt);
// Act & Assert
token.IsUsed.Should().BeFalse();
}
[Fact]
public void IsUsed_WhenUsed_ShouldReturnTrue()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(1);
var token = PasswordResetToken.Create(_userId, TestTokenHash, expiresAt);
token.MarkAsUsed();
// Act & Assert
token.IsUsed.Should().BeTrue();
}
[Fact]
public void IsValid_WithValidToken_ShouldReturnTrue()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(1);
var token = PasswordResetToken.Create(_userId, TestTokenHash, expiresAt);
// Act & Assert
token.IsValid.Should().BeTrue();
}
[Fact]
public void IsValid_WithExpiredToken_ShouldReturnFalse()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(-1);
var token = PasswordResetToken.Create(_userId, TestTokenHash, expiresAt);
// Act & Assert
token.IsValid.Should().BeFalse();
}
[Fact]
public void IsValid_WithUsedToken_ShouldReturnFalse()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(1);
var token = PasswordResetToken.Create(_userId, TestTokenHash, expiresAt);
token.MarkAsUsed();
// Act & Assert
token.IsValid.Should().BeFalse();
}
[Fact]
public void MarkAsUsed_WithValidToken_ShouldSetUsedAt()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(1);
var token = PasswordResetToken.Create(_userId, TestTokenHash, expiresAt);
// Act
token.MarkAsUsed();
// Assert
token.UsedAt.Should().NotBeNull();
token.UsedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
token.IsUsed.Should().BeTrue();
token.IsValid.Should().BeFalse();
}
[Fact]
public void MarkAsUsed_WithExpiredToken_ShouldThrowException()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(-1);
var token = PasswordResetToken.Create(_userId, TestTokenHash, expiresAt);
// Act
var act = () => token.MarkAsUsed();
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Token is not valid for password reset*");
}
[Fact]
public void MarkAsUsed_WithAlreadyUsedToken_ShouldThrowException()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(1);
var token = PasswordResetToken.Create(_userId, TestTokenHash, expiresAt);
token.MarkAsUsed();
// Act
var act = () => token.MarkAsUsed();
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Token is not valid for password reset*");
}
[Fact]
public void Create_WithShortExpiration_ShouldBeSecure()
{
// Arrange - Tokens should expire quickly for security (e.g., 1 hour)
var expiresAt = DateTime.UtcNow.AddHours(1);
// Act
var token = PasswordResetToken.Create(_userId, TestTokenHash, expiresAt);
// Assert
token.ExpiresAt.Should().BeBefore(DateTime.UtcNow.AddHours(2));
}
[Fact]
public void MarkAsUsed_PreventTokenReuse_ShouldEnforceSingleUse()
{
// Arrange
var expiresAt = DateTime.UtcNow.AddHours(1);
var token = PasswordResetToken.Create(_userId, TestTokenHash, expiresAt);
// Act - Use token once
token.MarkAsUsed();
// Assert - Subsequent use should fail
var act = () => token.MarkAsUsed();
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Token is not valid for password reset*");
}
}

View File

@@ -0,0 +1,120 @@
using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
using ColaFlow.Modules.Identity.Domain.Aggregates.Users;
using FluentAssertions;
using Xunit;
namespace ColaFlow.Modules.Identity.Domain.Tests.Entities;
public sealed class UserTenantRoleTests
{
private readonly UserId _userId = UserId.CreateUnique();
private readonly TenantId _tenantId = TenantId.CreateUnique();
private readonly Guid _assignedByUserId = Guid.NewGuid();
[Fact]
public void Create_WithValidData_ShouldSucceed()
{
// Arrange & Act
var userTenantRole = UserTenantRole.Create(
_userId,
_tenantId,
TenantRole.TenantMember,
_assignedByUserId);
// Assert
userTenantRole.Should().NotBeNull();
userTenantRole.Id.Should().NotBe(Guid.Empty);
userTenantRole.UserId.Should().Be(_userId);
userTenantRole.TenantId.Should().Be(_tenantId);
userTenantRole.Role.Should().Be(TenantRole.TenantMember);
userTenantRole.AssignedByUserId.Should().Be(_assignedByUserId);
userTenantRole.AssignedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
}
[Fact]
public void Create_WithoutAssignedByUserId_ShouldSucceed()
{
// Arrange & Act
var userTenantRole = UserTenantRole.Create(
_userId,
_tenantId,
TenantRole.TenantAdmin);
// Assert
userTenantRole.Should().NotBeNull();
userTenantRole.AssignedByUserId.Should().BeNull();
}
[Fact]
public void UpdateRole_WithValidRole_ShouldUpdate()
{
// Arrange
var userTenantRole = UserTenantRole.Create(
_userId,
_tenantId,
TenantRole.TenantMember);
var originalAssignedAt = userTenantRole.AssignedAt;
var updatedByUserId = Guid.NewGuid();
// Act
userTenantRole.UpdateRole(TenantRole.TenantAdmin, updatedByUserId);
// Assert
userTenantRole.Role.Should().Be(TenantRole.TenantAdmin);
userTenantRole.AssignedByUserId.Should().Be(updatedByUserId);
userTenantRole.AssignedAt.Should().Be(originalAssignedAt); // AssignedAt should NOT change
}
[Fact]
public void UpdateRole_WithSameRole_ShouldBeIdempotent()
{
// Arrange
var userTenantRole = UserTenantRole.Create(
_userId,
_tenantId,
TenantRole.TenantMember,
_assignedByUserId);
var originalAssignedByUserId = userTenantRole.AssignedByUserId;
var originalRole = userTenantRole.Role;
// Act
userTenantRole.UpdateRole(TenantRole.TenantMember, Guid.NewGuid());
// Assert - Should not update if role is the same
userTenantRole.Role.Should().Be(originalRole);
userTenantRole.AssignedByUserId.Should().Be(originalAssignedByUserId);
}
[Fact]
public void HasPermission_WithTenantOwner_ShouldReturnTrueForAllPermissions()
{
// Arrange
var userTenantRole = UserTenantRole.Create(
_userId,
_tenantId,
TenantRole.TenantOwner);
// Act & Assert
userTenantRole.HasPermission("read_projects").Should().BeTrue();
userTenantRole.HasPermission("write_projects").Should().BeTrue();
userTenantRole.HasPermission("delete_projects").Should().BeTrue();
userTenantRole.HasPermission("any_permission").Should().BeTrue();
}
[Fact]
public void HasPermission_WithAIAgent_ShouldReturnTrueForReadAndPreview()
{
// Arrange
var userTenantRole = UserTenantRole.Create(
_userId,
_tenantId,
TenantRole.AIAgent);
// Act & Assert
userTenantRole.HasPermission("read_projects").Should().BeTrue();
userTenantRole.HasPermission("read_users").Should().BeTrue();
userTenantRole.HasPermission("write_preview_changes").Should().BeTrue();
userTenantRole.HasPermission("write_direct_changes").Should().BeFalse();
userTenantRole.HasPermission("delete_projects").Should().BeFalse();
}
}