Files
social-media/backend/tests/Socialize.Tests/Approvals/ApprovalWorkflowRulesTests.cs
2026-05-01 14:23:37 -04:00

351 lines
12 KiB
C#

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<Guid> 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<AppDbContext>()
.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<AppDbContext>()
.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<AppDbContext>()
.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),
]));
}
}