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() .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() .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() .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() .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(); 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(); } [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() .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() .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(); } [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() .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() .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() .WithMessage("*Invitation has already been accepted*"); } }