using Microsoft.EntityFrameworkCore; using Socialize.Api.Data; using Socialize.Api.Modules.Approvals.Data; using Socialize.Api.Modules.Approvals.Services; namespace Socialize.Tests.Approvals; public class ApprovalWorkflowRulesTests { [Theory] [InlineData(ApprovalModes.Optional, true)] [InlineData(ApprovalModes.Required, true)] [InlineData(ApprovalModes.None, false)] [InlineData(ApprovalModes.MultiLevel, false)] public void CanCreateSingleStepApprovalRequest_matches_basic_modes(string approvalMode, bool expected) { bool actual = ApprovalWorkflowRules.CanCreateSingleStepApprovalRequest(approvalMode); Assert.Equal(expected, actual); } [Theory] [InlineData(ApprovalModes.Required, true)] [InlineData(ApprovalModes.MultiLevel, true)] [InlineData(ApprovalModes.Optional, false)] [InlineData(ApprovalModes.None, false)] public void BlocksManualApprovedOrScheduledStatus_matches_blocking_modes(string approvalMode, bool expected) { bool actual = ApprovalWorkflowRules.BlocksManualApprovedOrScheduledStatus(approvalMode); Assert.Equal(expected, actual); } [Theory] [InlineData("Approved", true)] [InlineData("Scheduled", true)] [InlineData("In approval", false)] [InlineData("Published", false)] public void IsApprovalCompletionStatus_only_matches_approval_gate_destinations(string status, bool expected) { bool actual = ApprovalWorkflowRules.IsApprovalCompletionStatus(status); Assert.Equal(expected, actual); } [Fact] public void GetFinalApprovalStatus_schedules_when_option_enabled_and_publish_date_exists() { string status = ApprovalWorkflowRules.GetFinalApprovalStatus( schedulePostsAutomaticallyOnApproval: true, plannedPublishDate: DateTimeOffset.UtcNow.AddDays(1)); Assert.Equal("Scheduled", status); } [Theory] [InlineData(false)] [InlineData(true)] public void GetFinalApprovalStatus_approves_when_auto_schedule_disabled_or_date_missing(bool scheduleAutomatically) { string status = ApprovalWorkflowRules.GetFinalApprovalStatus( scheduleAutomatically, plannedPublishDate: null); Assert.Equal("Approved", status); } [Theory] [InlineData(1, 1, true)] [InlineData(1, 2, false)] [InlineData(2, 2, true)] [InlineData(1, 0, true)] public void HasRequiredStepApprovals_enforces_configured_approver_count( int approvedDecisionCount, int requiredApproverCount, bool expected) { bool actual = ApprovalWorkflowRules.HasRequiredStepApprovals( approvedDecisionCount, requiredApproverCount); Assert.Equal(expected, actual); } [Fact] public void CanApproveWorkflowStep_allows_admin_for_any_step_target() { bool actual = ApprovalWorkflowRules.CanApproveWorkflowStep( isAdministrator: true, hasWorkspaceAccess: false, userRoles: [], userId: Guid.NewGuid(), targetType: ApprovalStepTargetTypes.Member, targetValue: Guid.NewGuid().ToString()); Assert.True(actual); } [Fact] public void CanApproveWorkflowStep_requires_role_target_match() { bool actual = ApprovalWorkflowRules.CanApproveWorkflowStep( isAdministrator: false, hasWorkspaceAccess: true, userRoles: ["manager"], userId: Guid.NewGuid(), targetType: ApprovalStepTargetTypes.Role, targetValue: "manager"); Assert.True(actual); } [Fact] public void CanApproveWorkflowStep_rejects_later_step_actor_without_target_match() { bool actual = ApprovalWorkflowRules.CanApproveWorkflowStep( isAdministrator: false, hasWorkspaceAccess: true, userRoles: ["provider"], userId: Guid.NewGuid(), targetType: ApprovalStepTargetTypes.Role, targetValue: "client"); Assert.False(actual); } [Fact] public void CanApproveWorkflowStep_requires_member_target_identity_match() { Guid assignedMemberId = Guid.NewGuid(); Guid secondAssignedMemberId = Guid.NewGuid(); bool matchingMember = ApprovalWorkflowRules.CanApproveWorkflowStep( isAdministrator: false, hasWorkspaceAccess: true, userRoles: ["workspace-member"], userId: assignedMemberId, targetType: ApprovalStepTargetTypes.Member, targetValue: $"{assignedMemberId},{secondAssignedMemberId}"); bool otherMember = ApprovalWorkflowRules.CanApproveWorkflowStep( isAdministrator: false, hasWorkspaceAccess: true, userRoles: ["workspace-member"], userId: Guid.NewGuid(), targetType: ApprovalStepTargetTypes.Member, targetValue: $"{assignedMemberId},{secondAssignedMemberId}"); Assert.True(matchingMember); Assert.False(otherMember); } [Fact] public void ParseMemberTargetIds_reads_distinct_comma_separated_member_ids() { Guid firstMemberId = Guid.NewGuid(); Guid secondMemberId = Guid.NewGuid(); IReadOnlyCollection memberIds = ApprovalWorkflowRules.ParseMemberTargetIds( $" {firstMemberId},not-a-guid,{secondMemberId},{firstMemberId} "); Assert.Equal([firstMemberId, secondMemberId], memberIds); } [Fact] public void FormatMemberTargetValue_stores_member_ids_stably() { Guid firstMemberId = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); Guid secondMemberId = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"); string targetValue = ApprovalWorkflowRules.FormatMemberTargetValue( [ secondMemberId, firstMemberId, secondMemberId, ]); Assert.Equal($"{firstMemberId},{secondMemberId}", targetValue); } [Fact] public void CanApproveWorkflowStep_matches_membership_targets() { bool clientMatchesClient = ApprovalWorkflowRules.CanApproveWorkflowStep( isAdministrator: false, hasWorkspaceAccess: true, userRoles: ["client"], userId: Guid.NewGuid(), targetType: ApprovalStepTargetTypes.Membership, targetValue: ApprovalMembershipTargets.Client); bool providerMatchesTeam = ApprovalWorkflowRules.CanApproveWorkflowStep( isAdministrator: false, hasWorkspaceAccess: true, userRoles: ["provider"], userId: Guid.NewGuid(), targetType: ApprovalStepTargetTypes.Membership, targetValue: ApprovalMembershipTargets.Team); bool clientDoesNotMatchTeam = ApprovalWorkflowRules.CanApproveWorkflowStep( isAdministrator: false, hasWorkspaceAccess: true, userRoles: ["client"], userId: Guid.NewGuid(), targetType: ApprovalStepTargetTypes.Membership, targetValue: ApprovalMembershipTargets.Team); Assert.True(clientMatchesClient); Assert.True(providerMatchesTeam); Assert.False(clientDoesNotMatchTeam); } [Theory] [InlineData(ApprovalStepTargetTypes.Role)] [InlineData(ApprovalStepTargetTypes.Membership)] [InlineData(ApprovalStepTargetTypes.Member)] public void IsValidTargetType_accepts_supported_target_types(string targetType) { bool valid = ApprovalStepConfigurationRules.IsValidTargetType(targetType); Assert.True(valid); } [Theory] [InlineData("")] [InlineData("Group")] [InlineData("role")] public void IsValidTargetType_rejects_unsupported_target_types(string targetType) { bool valid = ApprovalStepConfigurationRules.IsValidTargetType(targetType); Assert.False(valid); } [Theory] [InlineData("administrator")] [InlineData("manager")] [InlineData("workspace-member")] [InlineData("client")] [InlineData("provider")] public void IsValidRoleTarget_accepts_known_workspace_roles(string role) { bool valid = ApprovalStepConfigurationRules.IsValidRoleTarget(role); Assert.True(valid); } [Theory] [InlineData("")] [InlineData("developer")] [InlineData("owner")] public void IsValidRoleTarget_rejects_non_workspace_approval_roles(string role) { bool valid = ApprovalStepConfigurationRules.IsValidRoleTarget(role); Assert.False(valid); } [Theory] [InlineData(ApprovalMembershipTargets.Team)] [InlineData(ApprovalMembershipTargets.Client)] public void IsValidMembershipTarget_accepts_supported_memberships(string membership) { bool valid = ApprovalStepConfigurationRules.IsValidMembershipTarget(membership); Assert.True(valid); } [Theory] [InlineData("")] [InlineData("Provider")] [InlineData("External")] public void IsValidMembershipTarget_rejects_unsupported_memberships(string membership) { bool valid = ApprovalStepConfigurationRules.IsValidMembershipTarget(membership); Assert.False(valid); } [Fact] public void WorkspaceApprovalStepConfiguration_model_persists_workspace_ordering() { var options = new DbContextOptionsBuilder() .UseNpgsql("Host=localhost;Database=socialize_model_test") .Options; using var dbContext = new AppDbContext(options); var entity = dbContext.Model.FindEntityType(typeof(WorkspaceApprovalStepConfiguration)); Assert.NotNull(entity); Assert.Equal("WorkspaceApprovalStepConfigurations", entity.GetTableName()); Assert.Equal(128, entity.FindProperty(nameof(WorkspaceApprovalStepConfiguration.Name))?.GetMaxLength()); Assert.Equal(32, entity.FindProperty(nameof(WorkspaceApprovalStepConfiguration.TargetType))?.GetMaxLength()); Assert.Equal(128, entity.FindProperty(nameof(WorkspaceApprovalStepConfiguration.TargetValue))?.GetMaxLength()); Assert.Contains( entity.GetIndexes(), index => index.IsUnique && index.Properties.Select(property => property.Name).SequenceEqual( [ nameof(WorkspaceApprovalStepConfiguration.WorkspaceId), nameof(WorkspaceApprovalStepConfiguration.SortOrder), ])); } [Fact] public void ApprovalWorkflowInstance_model_allows_only_one_pending_workflow_per_content_item() { var options = new DbContextOptionsBuilder() .UseNpgsql("Host=localhost;Database=socialize_model_test") .Options; using var dbContext = new AppDbContext(options); var entity = dbContext.Model.FindEntityType(typeof(ApprovalWorkflowInstance)); Assert.NotNull(entity); Assert.Equal("ApprovalWorkflowInstances", entity.GetTableName()); Assert.Equal(64, entity.FindProperty(nameof(ApprovalWorkflowInstance.State))?.GetMaxLength()); Assert.Equal(64, entity.FindProperty(nameof(ApprovalWorkflowInstance.ApprovalMode))?.GetMaxLength()); Assert.Contains( entity.GetIndexes(), index => index.IsUnique && index.GetFilter() == "\"State\" = 'Pending'" && index.Properties.Select(property => property.Name).SequenceEqual( [ nameof(ApprovalWorkflowInstance.ContentItemId), nameof(ApprovalWorkflowInstance.State), ])); } [Fact] public void ApprovalRequest_model_persists_runtime_step_metadata() { var options = new DbContextOptionsBuilder() .UseNpgsql("Host=localhost;Database=socialize_model_test") .Options; using var dbContext = new AppDbContext(options); var entity = dbContext.Model.FindEntityType(typeof(ApprovalRequest)); Assert.NotNull(entity); Assert.Equal(32, entity.FindProperty(nameof(ApprovalRequest.WorkflowStepTargetType))?.GetMaxLength()); Assert.Equal(128, entity.FindProperty(nameof(ApprovalRequest.WorkflowStepTargetValue))?.GetMaxLength()); Assert.Contains( entity.GetIndexes(), index => index.Properties.Select(property => property.Name).SequenceEqual( [ nameof(ApprovalRequest.WorkflowInstanceId), ])); } }