324 lines
9.4 KiB
C#
324 lines
9.4 KiB
C#
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*");
|
|
}
|
|
}
|