403 lines
10 KiB
C#
403 lines
10 KiB
C#
using Xunit;
|
|
using FluentAssertions;
|
|
using IamService.Domain.AggregatesModel.AccessReviewAggregate;
|
|
using IamService.Domain.Events;
|
|
|
|
namespace IamService.UnitTests.Domain.AccessControl;
|
|
|
|
/// <summary>
|
|
/// EN: Unit tests for AccessReview aggregate root.
|
|
/// VI: Unit tests cho AccessReview aggregate root.
|
|
/// </summary>
|
|
public class AccessReviewTests
|
|
{
|
|
private readonly Guid _validOwnerId = Guid.NewGuid();
|
|
|
|
#region Creation Tests
|
|
|
|
[Fact]
|
|
public void Create_ValidParameters_CreatesAccessReviewInDraftStatus()
|
|
{
|
|
// Arrange
|
|
var dueDate = DateTime.UtcNow.AddDays(7);
|
|
|
|
// Act
|
|
var review = AccessReview.Create(
|
|
"Q1 Access Review",
|
|
"Quarterly access certification",
|
|
_validOwnerId,
|
|
"Organization:org-123",
|
|
dueDate);
|
|
|
|
// Assert
|
|
review.Should().NotBeNull();
|
|
review.Id.Should().NotBeEmpty();
|
|
review.Name.Should().Be("Q1 Access Review");
|
|
review.Description.Should().Be("Quarterly access certification");
|
|
review.OwnerId.Should().Be(_validOwnerId);
|
|
review.Scope.Should().Be("Organization:org-123");
|
|
review.Status.Should().Be(AccessReviewStatus.Draft);
|
|
review.DueDate.Should().Be(dueDate);
|
|
review.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
|
review.Items.Should().BeEmpty();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
public void Create_InvalidName_ThrowsArgumentException(string? name)
|
|
{
|
|
// Arrange & Act
|
|
var act = () => AccessReview.Create(
|
|
name!,
|
|
null,
|
|
_validOwnerId,
|
|
"Role:admin",
|
|
DateTime.UtcNow.AddDays(7));
|
|
|
|
// Assert
|
|
act.Should().Throw<ArgumentException>()
|
|
.WithMessage("*Name*empty*");
|
|
}
|
|
|
|
[Fact]
|
|
public void Create_EmptyOwnerId_ThrowsArgumentException()
|
|
{
|
|
// Arrange & Act
|
|
var act = () => AccessReview.Create(
|
|
"Test Review",
|
|
null,
|
|
Guid.Empty,
|
|
"Role:admin",
|
|
DateTime.UtcNow.AddDays(7));
|
|
|
|
// Assert
|
|
act.Should().Throw<ArgumentException>()
|
|
.WithMessage("*Owner ID*empty*");
|
|
}
|
|
|
|
[Fact]
|
|
public void Create_PastDueDate_ThrowsArgumentException()
|
|
{
|
|
// Arrange & Act
|
|
var act = () => AccessReview.Create(
|
|
"Test Review",
|
|
null,
|
|
_validOwnerId,
|
|
"Role:admin",
|
|
DateTime.UtcNow.AddDays(-1));
|
|
|
|
// Assert
|
|
act.Should().Throw<ArgumentException>()
|
|
.WithMessage("*Due date*future*");
|
|
}
|
|
|
|
[Fact]
|
|
public void Create_RaisesAccessReviewCreatedEvent()
|
|
{
|
|
// Act
|
|
var review = AccessReview.Create(
|
|
"Test Review",
|
|
null,
|
|
_validOwnerId,
|
|
"Role:admin",
|
|
DateTime.UtcNow.AddDays(7));
|
|
|
|
// Assert
|
|
review.DomainEvents.Should().ContainSingle();
|
|
review.DomainEvents.First().Should().BeOfType<AccessReviewCreatedEvent>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region AddItem Tests
|
|
|
|
[Fact]
|
|
public void AddItem_InDraftStatus_AddsReviewItem()
|
|
{
|
|
// Arrange
|
|
var review = CreateDraftReview();
|
|
var userId = Guid.NewGuid();
|
|
var resourceId = Guid.NewGuid();
|
|
|
|
// Act
|
|
var item = review.AddItem(userId, "Project", resourceId, "write");
|
|
|
|
// Assert
|
|
review.Items.Should().HaveCount(1);
|
|
item.UserId.Should().Be(userId);
|
|
item.ResourceType.Should().Be("Project");
|
|
item.Permission.Should().Be("write");
|
|
}
|
|
|
|
[Fact]
|
|
public void AddItem_AfterStart_ThrowsInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
var review = CreateDraftReview();
|
|
review.AddItem(Guid.NewGuid(), "Project", Guid.NewGuid(), "read");
|
|
review.Start();
|
|
|
|
// Act
|
|
var act = () => review.AddItem(Guid.NewGuid(), "Project", Guid.NewGuid(), "write");
|
|
|
|
// Assert
|
|
act.Should().Throw<InvalidOperationException>()
|
|
.WithMessage("*draft reviews*");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Start Tests
|
|
|
|
[Fact]
|
|
public void Start_WithItems_ChangesStatusToActive()
|
|
{
|
|
// Arrange
|
|
var review = CreateDraftReview();
|
|
review.AddItem(Guid.NewGuid(), "Project", Guid.NewGuid(), "read");
|
|
|
|
// Act
|
|
review.Start();
|
|
|
|
// Assert
|
|
review.Status.Should().Be(AccessReviewStatus.Active);
|
|
review.StartedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
|
}
|
|
|
|
[Fact]
|
|
public void Start_WithoutItems_ThrowsInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
var review = CreateDraftReview();
|
|
|
|
// Act
|
|
var act = () => review.Start();
|
|
|
|
// Assert
|
|
act.Should().Throw<InvalidOperationException>()
|
|
.WithMessage("*at least one item*");
|
|
}
|
|
|
|
[Fact]
|
|
public void Start_NotInDraft_ThrowsInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
var review = CreateDraftReview();
|
|
review.AddItem(Guid.NewGuid(), "Project", Guid.NewGuid(), "read");
|
|
review.Start();
|
|
|
|
// Act
|
|
var act = () => review.Start();
|
|
|
|
// Assert
|
|
act.Should().Throw<InvalidOperationException>()
|
|
.WithMessage("*draft reviews*");
|
|
}
|
|
|
|
[Fact]
|
|
public void Start_RaisesAccessReviewStartedEvent()
|
|
{
|
|
// Arrange
|
|
var review = CreateDraftReview();
|
|
review.AddItem(Guid.NewGuid(), "Project", Guid.NewGuid(), "read");
|
|
review.ClearDomainEvents();
|
|
|
|
// Act
|
|
review.Start();
|
|
|
|
// Assert
|
|
review.DomainEvents.Should().ContainSingle();
|
|
review.DomainEvents.First().Should().BeOfType<AccessReviewStartedEvent>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ReviewItem Tests
|
|
|
|
[Fact]
|
|
public void ReviewItem_Certify_SetsDecisionToCertify()
|
|
{
|
|
// Arrange
|
|
var review = CreateActiveReviewWithItem(out var item);
|
|
var reviewerId = Guid.NewGuid();
|
|
|
|
// Act
|
|
review.ReviewItem(item.Id, reviewerId, certify: true, "Approved access");
|
|
|
|
// Assert
|
|
item.Decision.Should().Be(ReviewDecision.Certify);
|
|
item.ReviewedByUserId.Should().Be(reviewerId);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReviewItem_Revoke_SetsDecisionToRevoke()
|
|
{
|
|
// Arrange
|
|
var review = CreateActiveReviewWithItem(out var item);
|
|
var reviewerId = Guid.NewGuid();
|
|
|
|
// Act
|
|
review.ReviewItem(item.Id, reviewerId, certify: false, "Access no longer needed");
|
|
|
|
// Assert
|
|
item.Decision.Should().Be(ReviewDecision.Revoke);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReviewItem_NotActive_ThrowsInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
var review = CreateDraftReview();
|
|
review.AddItem(Guid.NewGuid(), "Project", Guid.NewGuid(), "read");
|
|
|
|
// Act
|
|
var act = () => review.ReviewItem(Guid.NewGuid(), Guid.NewGuid(), true);
|
|
|
|
// Assert
|
|
act.Should().Throw<InvalidOperationException>()
|
|
.WithMessage("*active reviews*");
|
|
}
|
|
|
|
[Fact]
|
|
public void ReviewItem_InvalidItemId_ThrowsInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
var review = CreateActiveReviewWithItem(out _);
|
|
|
|
// Act
|
|
var act = () => review.ReviewItem(Guid.NewGuid(), Guid.NewGuid(), true);
|
|
|
|
// Assert
|
|
act.Should().Throw<InvalidOperationException>()
|
|
.WithMessage("*Item not found*");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Complete Tests
|
|
|
|
[Fact]
|
|
public void Complete_AllItemsReviewed_ChangesStatusToCompleted()
|
|
{
|
|
// Arrange
|
|
var review = CreateActiveReviewWithItem(out var item);
|
|
review.ReviewItem(item.Id, Guid.NewGuid(), true);
|
|
|
|
// Act
|
|
review.Complete();
|
|
|
|
// Assert
|
|
review.Status.Should().Be(AccessReviewStatus.Completed);
|
|
review.CompletedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
|
}
|
|
|
|
[Fact]
|
|
public void Complete_PendingItems_ThrowsInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
var review = CreateActiveReviewWithItem(out _);
|
|
|
|
// Act
|
|
var act = () => review.Complete();
|
|
|
|
// Assert
|
|
act.Should().Throw<InvalidOperationException>()
|
|
.WithMessage("*pending items*");
|
|
}
|
|
|
|
[Fact]
|
|
public void Complete_NotActive_ThrowsInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
var review = CreateDraftReview();
|
|
|
|
// Act
|
|
var act = () => review.Complete();
|
|
|
|
// Assert
|
|
act.Should().Throw<InvalidOperationException>()
|
|
.WithMessage("*active reviews*");
|
|
}
|
|
|
|
[Fact]
|
|
public void Complete_RaisesAccessReviewCompletedEvent()
|
|
{
|
|
// Arrange
|
|
var review = CreateActiveReviewWithItem(out var item);
|
|
review.ReviewItem(item.Id, Guid.NewGuid(), true);
|
|
review.ClearDomainEvents();
|
|
|
|
// Act
|
|
review.Complete();
|
|
|
|
// Assert
|
|
review.DomainEvents.Should().ContainSingle();
|
|
review.DomainEvents.First().Should().BeOfType<AccessReviewCompletedEvent>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Cancel Tests
|
|
|
|
[Fact]
|
|
public void Cancel_DraftReview_ChangesStatusToCancelled()
|
|
{
|
|
// Arrange
|
|
var review = CreateDraftReview();
|
|
|
|
// Act
|
|
review.Cancel();
|
|
|
|
// Assert
|
|
review.Status.Should().Be(AccessReviewStatus.Cancelled);
|
|
}
|
|
|
|
[Fact]
|
|
public void Cancel_ActiveReview_ChangesStatusToCancelled()
|
|
{
|
|
// Arrange
|
|
var review = CreateActiveReviewWithItem(out _);
|
|
|
|
// Act
|
|
review.Cancel();
|
|
|
|
// Assert
|
|
review.Status.Should().Be(AccessReviewStatus.Cancelled);
|
|
}
|
|
|
|
[Fact]
|
|
public void Cancel_CompletedReview_ThrowsInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
var review = CreateActiveReviewWithItem(out var item);
|
|
review.ReviewItem(item.Id, Guid.NewGuid(), true);
|
|
review.Complete();
|
|
|
|
// Act
|
|
var act = () => review.Cancel();
|
|
|
|
// Assert
|
|
act.Should().Throw<InvalidOperationException>()
|
|
.WithMessage("*completed review*");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private AccessReview CreateDraftReview() =>
|
|
AccessReview.Create("Test Review", null, _validOwnerId, "Role:admin", DateTime.UtcNow.AddDays(7));
|
|
|
|
private AccessReview CreateActiveReviewWithItem(out AccessReviewItem item)
|
|
{
|
|
var review = CreateDraftReview();
|
|
item = review.AddItem(Guid.NewGuid(), "Project", Guid.NewGuid(), "read");
|
|
review.Start();
|
|
return review;
|
|
}
|
|
|
|
#endregion
|
|
}
|