using ColaFlow.Modules.Mcp.Domain.Entities; using ColaFlow.Modules.Mcp.Domain.Events; using ColaFlow.Modules.Mcp.Domain.ValueObjects; using Xunit; namespace ColaFlow.Modules.Mcp.Tests.Domain; public class PendingChangeTests { private static DiffPreview CreateValidDiff() { return DiffPreview.ForCreate( entityType: "Epic", afterData: "{\"title\": \"New Epic\"}" ); } [Fact] public void Create_ValidInput_Success() { // Arrange var diff = CreateValidDiff(); var tenantId = Guid.NewGuid(); var apiKeyId = Guid.NewGuid(); // Act var pendingChange = PendingChange.Create( toolName: "create_epic", diff: diff, tenantId: tenantId, apiKeyId: apiKeyId ); // Assert Assert.NotEqual(Guid.Empty, pendingChange.Id); Assert.Equal("create_epic", pendingChange.ToolName); Assert.Equal(diff, pendingChange.Diff); Assert.Equal(tenantId, pendingChange.TenantId); Assert.Equal(apiKeyId, pendingChange.ApiKeyId); Assert.Equal(PendingChangeStatus.PendingApproval, pendingChange.Status); Assert.True(pendingChange.ExpiresAt > DateTime.UtcNow); Assert.Null(pendingChange.ApprovedBy); Assert.Null(pendingChange.ApprovedAt); Assert.Null(pendingChange.RejectedBy); Assert.Null(pendingChange.RejectedAt); } [Fact] public void Create_RaisesPendingChangeCreatedEvent() { // Arrange var diff = CreateValidDiff(); var tenantId = Guid.NewGuid(); var apiKeyId = Guid.NewGuid(); // Act var pendingChange = PendingChange.Create("create_epic", diff, tenantId, apiKeyId); // Assert Assert.Single(pendingChange.DomainEvents); var domainEvent = pendingChange.DomainEvents.First(); Assert.IsType(domainEvent); var createdEvent = (PendingChangeCreatedEvent)domainEvent; Assert.Equal(pendingChange.Id, createdEvent.PendingChangeId); Assert.Equal("create_epic", createdEvent.ToolName); Assert.Equal("Epic", createdEvent.EntityType); Assert.Equal("CREATE", createdEvent.Operation); } [Fact] public void Create_EmptyToolName_ThrowsException() { // Arrange var diff = CreateValidDiff(); // Act & Assert Assert.Throws(() => PendingChange.Create( toolName: "", diff: diff, tenantId: Guid.NewGuid(), apiKeyId: Guid.NewGuid() )); } [Fact] public void Create_NullDiff_ThrowsException() { // Act & Assert Assert.Throws(() => PendingChange.Create( toolName: "create_epic", diff: null!, tenantId: Guid.NewGuid(), apiKeyId: Guid.NewGuid() )); } [Fact] public void Create_EmptyTenantId_ThrowsException() { // Arrange var diff = CreateValidDiff(); // Act & Assert Assert.Throws(() => PendingChange.Create( toolName: "create_epic", diff: diff, tenantId: Guid.Empty, apiKeyId: Guid.NewGuid() )); } [Fact] public void Create_CustomExpirationHours_SetsCorrectExpiration() { // Arrange var diff = CreateValidDiff(); var before = DateTime.UtcNow; // Act var pendingChange = PendingChange.Create( toolName: "create_epic", diff: diff, tenantId: Guid.NewGuid(), apiKeyId: Guid.NewGuid(), expirationHours: 48 ); // Assert var after = DateTime.UtcNow; Assert.True(pendingChange.ExpiresAt >= before.AddHours(48)); Assert.True(pendingChange.ExpiresAt <= after.AddHours(48)); } [Fact] public void Approve_PendingChange_Success() { // Arrange var pendingChange = PendingChange.Create( "create_epic", CreateValidDiff(), Guid.NewGuid(), Guid.NewGuid() ); var approverId = Guid.NewGuid(); pendingChange.ClearDomainEvents(); // Clear creation event // Act pendingChange.Approve(approverId); // Assert Assert.Equal(PendingChangeStatus.Approved, pendingChange.Status); Assert.Equal(approverId, pendingChange.ApprovedBy); Assert.NotNull(pendingChange.ApprovedAt); Assert.True(pendingChange.ApprovedAt <= DateTime.UtcNow); } [Fact] public void Approve_RaisesPendingChangeApprovedEvent() { // Arrange var pendingChange = PendingChange.Create( "create_epic", CreateValidDiff(), Guid.NewGuid(), Guid.NewGuid() ); var approverId = Guid.NewGuid(); pendingChange.ClearDomainEvents(); // Act pendingChange.Approve(approverId); // Assert Assert.Single(pendingChange.DomainEvents); var domainEvent = pendingChange.DomainEvents.First(); Assert.IsType(domainEvent); var approvedEvent = (PendingChangeApprovedEvent)domainEvent; Assert.Equal(pendingChange.Id, approvedEvent.PendingChangeId); Assert.Equal(approverId, approvedEvent.ApprovedBy); } [Fact] public void Approve_AlreadyApproved_ThrowsException() { // Arrange var pendingChange = PendingChange.Create( "create_epic", CreateValidDiff(), Guid.NewGuid(), Guid.NewGuid() ); pendingChange.Approve(Guid.NewGuid()); // Act & Assert Assert.Throws( () => pendingChange.Approve(Guid.NewGuid())); } [Fact] public void Approve_Rejected_ThrowsException() { // Arrange var pendingChange = PendingChange.Create( "create_epic", CreateValidDiff(), Guid.NewGuid(), Guid.NewGuid() ); pendingChange.Reject(Guid.NewGuid(), "Not valid"); // Act & Assert Assert.Throws( () => pendingChange.Approve(Guid.NewGuid())); } [Fact] public void Reject_PendingChange_Success() { // Arrange var pendingChange = PendingChange.Create( "create_epic", CreateValidDiff(), Guid.NewGuid(), Guid.NewGuid() ); var rejectorId = Guid.NewGuid(); var reason = "Invalid data"; pendingChange.ClearDomainEvents(); // Act pendingChange.Reject(rejectorId, reason); // Assert Assert.Equal(PendingChangeStatus.Rejected, pendingChange.Status); Assert.Equal(rejectorId, pendingChange.RejectedBy); Assert.Equal(reason, pendingChange.RejectionReason); Assert.NotNull(pendingChange.RejectedAt); Assert.True(pendingChange.RejectedAt <= DateTime.UtcNow); } [Fact] public void Reject_RaisesPendingChangeRejectedEvent() { // Arrange var pendingChange = PendingChange.Create( "create_epic", CreateValidDiff(), Guid.NewGuid(), Guid.NewGuid() ); var rejectorId = Guid.NewGuid(); pendingChange.ClearDomainEvents(); // Act pendingChange.Reject(rejectorId, "Invalid"); // Assert Assert.Single(pendingChange.DomainEvents); var domainEvent = pendingChange.DomainEvents.First(); Assert.IsType(domainEvent); var rejectedEvent = (PendingChangeRejectedEvent)domainEvent; Assert.Equal(pendingChange.Id, rejectedEvent.PendingChangeId); Assert.Equal(rejectorId, rejectedEvent.RejectedBy); Assert.Equal("Invalid", rejectedEvent.Reason); } [Fact] public void Reject_EmptyReason_ThrowsException() { // Arrange var pendingChange = PendingChange.Create( "create_epic", CreateValidDiff(), Guid.NewGuid(), Guid.NewGuid() ); // Act & Assert Assert.Throws( () => pendingChange.Reject(Guid.NewGuid(), "")); } [Fact] public void Reject_AlreadyApproved_ThrowsException() { // Arrange var pendingChange = PendingChange.Create( "create_epic", CreateValidDiff(), Guid.NewGuid(), Guid.NewGuid() ); pendingChange.Approve(Guid.NewGuid()); // Act & Assert Assert.Throws( () => pendingChange.Reject(Guid.NewGuid(), "Too late")); } [Fact] public void Expire_PendingChange_Success() { // Arrange var pendingChange = PendingChange.Create( "create_epic", CreateValidDiff(), Guid.NewGuid(), Guid.NewGuid(), expirationHours: 1 ); // Use reflection to set ExpiresAt to the past (simulating expiration) var expiresAtProperty = typeof(PendingChange).GetProperty("ExpiresAt"); expiresAtProperty!.SetValue(pendingChange, DateTime.UtcNow.AddHours(-1)); pendingChange.ClearDomainEvents(); // Act pendingChange.Expire(); // Assert Assert.Equal(PendingChangeStatus.Expired, pendingChange.Status); } [Fact] public void Expire_RaisesPendingChangeExpiredEvent() { // Arrange var pendingChange = PendingChange.Create( "create_epic", CreateValidDiff(), Guid.NewGuid(), Guid.NewGuid(), expirationHours: 1 ); // Use reflection to set ExpiresAt to the past var expiresAtProperty = typeof(PendingChange).GetProperty("ExpiresAt"); expiresAtProperty!.SetValue(pendingChange, DateTime.UtcNow.AddHours(-1)); pendingChange.ClearDomainEvents(); // Act pendingChange.Expire(); // Assert Assert.Single(pendingChange.DomainEvents); Assert.IsType(pendingChange.DomainEvents.First()); } [Fact] public void Expire_NotYetExpired_ThrowsException() { // Arrange var pendingChange = PendingChange.Create( "create_epic", CreateValidDiff(), Guid.NewGuid(), Guid.NewGuid(), expirationHours: 24 ); // Act & Assert Assert.Throws(() => pendingChange.Expire()); } [Fact] public void Expire_AlreadyApproved_DoesNothing() { // Arrange var pendingChange = PendingChange.Create( "create_epic", CreateValidDiff(), Guid.NewGuid(), Guid.NewGuid(), expirationHours: 1 ); pendingChange.Approve(Guid.NewGuid()); var statusBefore = pendingChange.Status; // Use reflection to set ExpiresAt to the past var expiresAtProperty = typeof(PendingChange).GetProperty("ExpiresAt"); expiresAtProperty!.SetValue(pendingChange, DateTime.UtcNow.AddHours(-1)); pendingChange.ClearDomainEvents(); // Act pendingChange.Expire(); // Assert Assert.Equal(statusBefore, pendingChange.Status); // Still Approved Assert.Empty(pendingChange.DomainEvents); // No event raised } [Fact] public void MarkAsApplied_ApprovedChange_Success() { // Arrange var pendingChange = PendingChange.Create( "create_epic", CreateValidDiff(), Guid.NewGuid(), Guid.NewGuid() ); pendingChange.Approve(Guid.NewGuid()); pendingChange.ClearDomainEvents(); // Act pendingChange.MarkAsApplied("Successfully created Epic COLA-1"); // Assert Assert.Equal(PendingChangeStatus.Applied, pendingChange.Status); Assert.NotNull(pendingChange.AppliedAt); Assert.Equal("Successfully created Epic COLA-1", pendingChange.ApplicationResult); } [Fact] public void MarkAsApplied_RaisesPendingChangeAppliedEvent() { // Arrange var pendingChange = PendingChange.Create( "create_epic", CreateValidDiff(), Guid.NewGuid(), Guid.NewGuid() ); pendingChange.Approve(Guid.NewGuid()); pendingChange.ClearDomainEvents(); // Act pendingChange.MarkAsApplied("Success"); // Assert Assert.Single(pendingChange.DomainEvents); Assert.IsType(pendingChange.DomainEvents.First()); } [Fact] public void MarkAsApplied_NotApproved_ThrowsException() { // Arrange var pendingChange = PendingChange.Create( "create_epic", CreateValidDiff(), Guid.NewGuid(), Guid.NewGuid() ); // Act & Assert Assert.Throws( () => pendingChange.MarkAsApplied("Success")); } [Fact] public void IsExpired_ExpiredChange_ReturnsTrue() { // Arrange var pendingChange = PendingChange.Create( "create_epic", CreateValidDiff(), Guid.NewGuid(), Guid.NewGuid(), expirationHours: 1 ); // Use reflection to set ExpiresAt to the past var expiresAtProperty = typeof(PendingChange).GetProperty("ExpiresAt"); expiresAtProperty!.SetValue(pendingChange, DateTime.UtcNow.AddHours(-1)); // Act & Assert Assert.True(pendingChange.IsExpired()); } [Fact] public void IsExpired_NotExpiredChange_ReturnsFalse() { // Arrange var pendingChange = PendingChange.Create( "create_epic", CreateValidDiff(), Guid.NewGuid(), Guid.NewGuid(), expirationHours: 24 ); // Act & Assert Assert.False(pendingChange.IsExpired()); } [Fact] public void CanBeApproved_PendingAndNotExpired_ReturnsTrue() { // Arrange var pendingChange = PendingChange.Create( "create_epic", CreateValidDiff(), Guid.NewGuid(), Guid.NewGuid(), expirationHours: 24 ); // Act & Assert Assert.True(pendingChange.CanBeApproved()); } [Fact] public void CanBeApproved_Expired_ReturnsFalse() { // Arrange var pendingChange = PendingChange.Create( "create_epic", CreateValidDiff(), Guid.NewGuid(), Guid.NewGuid(), expirationHours: 1 ); // Use reflection to set ExpiresAt to the past var expiresAtProperty = typeof(PendingChange).GetProperty("ExpiresAt"); expiresAtProperty!.SetValue(pendingChange, DateTime.UtcNow.AddHours(-1)); // Act & Assert Assert.False(pendingChange.CanBeApproved()); } [Fact] public void CanBeApproved_AlreadyApproved_ReturnsFalse() { // Arrange var pendingChange = PendingChange.Create( "create_epic", CreateValidDiff(), Guid.NewGuid(), Guid.NewGuid() ); pendingChange.Approve(Guid.NewGuid()); // Act & Assert Assert.False(pendingChange.CanBeApproved()); } [Fact] public void GetSummary_ReturnsFormattedString() { // Arrange var diff = DiffPreview.ForCreate("Epic", "{}", "COLA-1"); var pendingChange = PendingChange.Create( "create_epic", diff, Guid.NewGuid(), Guid.NewGuid() ); // Act var summary = pendingChange.GetSummary(); // Assert Assert.Contains("create_epic", summary); Assert.Contains("CREATE Epic (COLA-1)", summary); Assert.Contains("PendingApproval", summary); } }