319 lines
9.6 KiB
C#
319 lines
9.6 KiB
C#
using Xunit;
|
|
using FluentAssertions;
|
|
using IamService.Domain.AggregatesModel.VerificationAggregate;
|
|
using IamService.Domain.Events;
|
|
|
|
namespace IamService.UnitTests.Domain.Verification;
|
|
|
|
/// <summary>
|
|
/// EN: Unit tests for IdentityVerification aggregate root.
|
|
/// VI: Unit tests cho IdentityVerification aggregate root.
|
|
/// </summary>
|
|
public class IdentityVerificationTests
|
|
{
|
|
private readonly Guid _validUserId = Guid.NewGuid();
|
|
|
|
#region Phone Verification Creation Tests
|
|
|
|
[Fact]
|
|
public void CreatePhoneVerification_ValidParameters_CreatesVerificationAndOtp()
|
|
{
|
|
// Arrange & Act
|
|
var (verification, otp) = IdentityVerification.CreatePhoneVerification(
|
|
_validUserId, "+84901234567");
|
|
|
|
// Assert
|
|
verification.Should().NotBeNull();
|
|
verification.Id.Should().NotBeEmpty();
|
|
verification.UserId.Should().Be(_validUserId);
|
|
verification.Type.Should().Be(VerificationType.Phone);
|
|
verification.Status.Should().Be(VerificationStatus.Pending);
|
|
verification.VerificationData.Should().Be("+84901234567");
|
|
verification.VerificationCodeHash.Should().NotBeNullOrEmpty();
|
|
verification.RequestedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
|
verification.ExpiresAt.Should().BeAfter(DateTime.UtcNow);
|
|
verification.AttemptCount.Should().Be(0);
|
|
otp.Should().HaveLength(6);
|
|
otp.Should().MatchRegex("^[0-9]{6}$");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
public void CreatePhoneVerification_InvalidPhoneNumber_ThrowsArgumentException(string? phoneNumber)
|
|
{
|
|
// Arrange & Act
|
|
var act = () => IdentityVerification.CreatePhoneVerification(_validUserId, phoneNumber!);
|
|
|
|
// Assert
|
|
act.Should().Throw<ArgumentException>()
|
|
.WithMessage("*Phone number*empty*");
|
|
}
|
|
|
|
[Fact]
|
|
public void CreatePhoneVerification_EmptyUserId_ThrowsArgumentException()
|
|
{
|
|
// Arrange & Act
|
|
var act = () => IdentityVerification.CreatePhoneVerification(Guid.Empty, "+84901234567");
|
|
|
|
// Assert
|
|
act.Should().Throw<ArgumentException>()
|
|
.WithMessage("*User ID*empty*");
|
|
}
|
|
|
|
[Fact]
|
|
public void CreatePhoneVerification_RaisesVerificationRequestedEvent()
|
|
{
|
|
// Arrange & Act
|
|
var (verification, _) = IdentityVerification.CreatePhoneVerification(_validUserId, "+84901234567");
|
|
|
|
// Assert
|
|
verification.DomainEvents.Should().ContainSingle();
|
|
verification.DomainEvents.First().Should().BeOfType<VerificationRequestedEvent>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Document Verification Creation Tests
|
|
|
|
[Fact]
|
|
public void CreateDocumentVerification_ValidParameters_CreatesVerification()
|
|
{
|
|
// Arrange & Act
|
|
var verification = IdentityVerification.CreateDocumentVerification(
|
|
_validUserId,
|
|
"https://storage.example.com/documents/id-card.jpg",
|
|
"ID_CARD");
|
|
|
|
// Assert
|
|
verification.Should().NotBeNull();
|
|
verification.Id.Should().NotBeEmpty();
|
|
verification.UserId.Should().Be(_validUserId);
|
|
verification.Type.Should().Be(VerificationType.Document);
|
|
verification.Status.Should().Be(VerificationStatus.InProgress);
|
|
verification.VerificationData.Should().Be("https://storage.example.com/documents/id-card.jpg");
|
|
verification.Metadata.Should().Contain("ID_CARD");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
public void CreateDocumentVerification_InvalidDocumentUrl_ThrowsArgumentException(string? documentUrl)
|
|
{
|
|
// Arrange & Act
|
|
var act = () => IdentityVerification.CreateDocumentVerification(_validUserId, documentUrl!);
|
|
|
|
// Assert
|
|
act.Should().Throw<ArgumentException>()
|
|
.WithMessage("*Document URL*empty*");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region VerifyCode Tests
|
|
|
|
[Fact]
|
|
public void VerifyCode_CorrectCode_ReturnsTrue()
|
|
{
|
|
// Arrange
|
|
var (verification, otp) = IdentityVerification.CreatePhoneVerification(_validUserId, "+84901234567");
|
|
|
|
// Act
|
|
var result = verification.VerifyCode(otp);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
verification.Status.Should().Be(VerificationStatus.Verified);
|
|
verification.VerifiedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
|
}
|
|
|
|
[Fact]
|
|
public void VerifyCode_IncorrectCode_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var (verification, _) = IdentityVerification.CreatePhoneVerification(_validUserId, "+84901234567");
|
|
|
|
// Act
|
|
var result = verification.VerifyCode("000000");
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
verification.Status.Should().Be(VerificationStatus.Pending);
|
|
verification.AttemptCount.Should().Be(1);
|
|
}
|
|
|
|
[Fact]
|
|
public void VerifyCode_ExceedsMaxAttempts_RejectsVerification()
|
|
{
|
|
// Arrange
|
|
var (verification, _) = IdentityVerification.CreatePhoneVerification(_validUserId, "+84901234567");
|
|
|
|
// Act - Try 6 times (max is 5)
|
|
for (int i = 0; i < 6; i++)
|
|
{
|
|
verification.VerifyCode("000000");
|
|
}
|
|
|
|
// Assert
|
|
verification.Status.Should().Be(VerificationStatus.Rejected);
|
|
verification.RejectionReason.Should().Contain("Maximum attempts");
|
|
}
|
|
|
|
[Fact]
|
|
public void VerifyCode_AlreadyVerified_ThrowsInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
var (verification, otp) = IdentityVerification.CreatePhoneVerification(_validUserId, "+84901234567");
|
|
verification.VerifyCode(otp); // Verify first
|
|
|
|
// Act
|
|
var act = () => verification.VerifyCode("000000");
|
|
|
|
// Assert
|
|
act.Should().Throw<InvalidOperationException>()
|
|
.WithMessage("*final status*");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region MarkAsVerified Tests
|
|
|
|
[Fact]
|
|
public void MarkAsVerified_PendingStatus_ChangesToVerified()
|
|
{
|
|
// Arrange
|
|
var verification = IdentityVerification.CreateDocumentVerification(
|
|
_validUserId, "https://example.com/doc.jpg");
|
|
verification.ClearDomainEvents();
|
|
|
|
// Act
|
|
verification.MarkAsVerified();
|
|
|
|
// Assert
|
|
verification.Status.Should().Be(VerificationStatus.Verified);
|
|
verification.VerifiedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
|
}
|
|
|
|
[Fact]
|
|
public void MarkAsVerified_RaisesVerificationCompletedEvent()
|
|
{
|
|
// Arrange
|
|
var verification = IdentityVerification.CreateDocumentVerification(
|
|
_validUserId, "https://example.com/doc.jpg");
|
|
verification.ClearDomainEvents();
|
|
|
|
// Act
|
|
verification.MarkAsVerified();
|
|
|
|
// Assert
|
|
verification.DomainEvents.Should().ContainSingle();
|
|
verification.DomainEvents.First().Should().BeOfType<VerificationCompletedEvent>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region MarkAsRejected Tests
|
|
|
|
[Fact]
|
|
public void MarkAsRejected_PendingStatus_ChangesToRejected()
|
|
{
|
|
// Arrange
|
|
var verification = IdentityVerification.CreateDocumentVerification(
|
|
_validUserId, "https://example.com/doc.jpg");
|
|
|
|
// Act
|
|
verification.MarkAsRejected("Document is blurry");
|
|
|
|
// Assert
|
|
verification.Status.Should().Be(VerificationStatus.Rejected);
|
|
verification.RejectionReason.Should().Be("Document is blurry");
|
|
}
|
|
|
|
[Fact]
|
|
public void MarkAsRejected_AlreadyFinal_ThrowsInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
var (verification, otp) = IdentityVerification.CreatePhoneVerification(_validUserId, "+84901234567");
|
|
verification.VerifyCode(otp);
|
|
|
|
// Act
|
|
var act = () => verification.MarkAsRejected("Some reason");
|
|
|
|
// Assert
|
|
act.Should().Throw<InvalidOperationException>()
|
|
.WithMessage("*final status*");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Cancel Tests
|
|
|
|
[Fact]
|
|
public void Cancel_PendingStatus_ChangesToCancelled()
|
|
{
|
|
// Arrange
|
|
var (verification, _) = IdentityVerification.CreatePhoneVerification(_validUserId, "+84901234567");
|
|
|
|
// Act
|
|
verification.Cancel();
|
|
|
|
// Assert
|
|
verification.Status.Should().Be(VerificationStatus.Cancelled);
|
|
}
|
|
|
|
[Fact]
|
|
public void Cancel_FinalStatus_ThrowsInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
var (verification, otp) = IdentityVerification.CreatePhoneVerification(_validUserId, "+84901234567");
|
|
verification.VerifyCode(otp);
|
|
|
|
// Act
|
|
var act = () => verification.Cancel();
|
|
|
|
// Assert
|
|
act.Should().Throw<InvalidOperationException>()
|
|
.WithMessage("*final status*");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Properties Tests
|
|
|
|
[Fact]
|
|
public void IsExpired_BeforeExpiration_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var (verification, _) = IdentityVerification.CreatePhoneVerification(_validUserId, "+84901234567");
|
|
|
|
// Assert
|
|
verification.IsExpired.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void CanRetry_UnderMaxAttempts_ReturnsTrue()
|
|
{
|
|
// Arrange
|
|
var (verification, _) = IdentityVerification.CreatePhoneVerification(_validUserId, "+84901234567");
|
|
verification.VerifyCode("000000"); // 1 failed attempt
|
|
|
|
// Assert
|
|
verification.CanRetry.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void CanRetry_AfterVerified_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var (verification, otp) = IdentityVerification.CreatePhoneVerification(_validUserId, "+84901234567");
|
|
verification.VerifyCode(otp);
|
|
|
|
// Assert
|
|
verification.CanRetry.Should().BeFalse();
|
|
}
|
|
|
|
#endregion
|
|
}
|