339 lines
12 KiB
C#
339 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.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),
|
|
]));
|
|
}
|
|
}
|