Skip to content

Commit ade4121

Browse files
committed
test(governance): reorganize test support
1 parent f1fc3d4 commit ade4121

24 files changed

Lines changed: 1622 additions & 1454 deletions
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
using ModularityKit.Mutator.Abstractions.Context;
2+
using ModularityKit.Mutator.Governance.Abstractions.Approval.Model;
3+
using ModularityKit.Mutator.Governance.Abstractions.Lifecycle.Model;
4+
using ModularityKit.Mutator.Governance.Abstractions.Requests.Decisions;
5+
using ModularityKit.Mutator.Governance.Runtime.Approval.Execution;
6+
using ModularityKit.Mutator.Governance.Runtime.Storage;
7+
using ModularityKit.Mutator.Governance.Tests.TestSupport.Approval.Workflow;
8+
using Xunit;
9+
10+
namespace ModularityKit.Mutator.Governance.Tests.Approval;
11+
12+
public sealed partial class MutationRequestApprovalWorkflowTests
13+
{
14+
[Fact]
15+
public void PendingApproval_maps_id_role_group_quorum_and_expiration_targets()
16+
{
17+
var expiresAt = DateTimeOffset.UtcNow.AddHours(1);
18+
19+
var request = MutationRequestApprovalWorkflowTestSupport.CreateLinearApprovalRequest();
20+
21+
Assert.Equal(MutationRequestStatus.Pending, request.Status);
22+
Assert.Equal(PendingMutationReason.Approval, request.PendingReason);
23+
Assert.Equal(6, request.ApprovalRequirements.Count);
24+
25+
var securityApprovals = request.ApprovalRequirements
26+
.Where(requirement => requirement.ApprovalGroupId == "security-quorum")
27+
.OrderBy(requirement => requirement.ApproverId)
28+
.ToList();
29+
30+
Assert.Equal(3, securityApprovals.Count);
31+
32+
void Action(MutationApprovalRequirement requirement)
33+
{
34+
Assert.Equal(2, requirement.RequiredApprovals);
35+
Assert.Equal(expiresAt, requirement.ExpiresAt);
36+
Assert.Equal(2, requirement.StepOrder);
37+
}
38+
39+
Assert.All(securityApprovals, Action);
40+
41+
var financeApproval = request.ApprovalRequirements.Single(requirement => requirement.ApproverRole == "finance-approver");
42+
Assert.Equal(3, financeApproval.StepOrder);
43+
44+
var operationsApproval = request.ApprovalRequirements.Single(requirement => requirement.ApproverGroup == "ops-oncall");
45+
Assert.Equal(4, operationsApproval.StepOrder);
46+
}
47+
48+
[Fact]
49+
public async Task ApproveRequirement_supports_quorum_groups_and_marks_remaining_group_requirements_satisfied()
50+
{
51+
var store = new InMemoryMutationRequestStore();
52+
var manager = new MutationRequestApprovalWorkflowManager(store);
53+
var request = await store.Create(MutationRequestApprovalWorkflowTestSupport.CreateQuorumApprovalRequest());
54+
55+
var aliceApproval = request.ApprovalRequirements.Single(requirement => requirement.ApproverId == "alice");
56+
var afterAlice = await manager.ApproveRequirement(
57+
request.RequestId,
58+
aliceApproval.ApprovalId,
59+
MutationContext.User("alice", "Alice", "Manager approved"));
60+
61+
Assert.Equal(MutationRequestStatus.Pending, afterAlice.Status);
62+
63+
var bobApproval = afterAlice.ApprovalRequirements.Single(requirement => requirement.ApproverId == "bob");
64+
var afterBob = await manager.ApproveRequirement(
65+
request.RequestId,
66+
bobApproval.ApprovalId,
67+
MutationContext.User("bob", "Bob", "Security approved"));
68+
69+
Assert.Equal(MutationRequestStatus.Pending, afterBob.Status);
70+
71+
var carolApproval = afterBob.ApprovalRequirements.Single(requirement => requirement.ApproverId == "carol");
72+
var afterCarol = await manager.ApproveRequirement(
73+
request.RequestId,
74+
carolApproval.ApprovalId,
75+
MutationContext.User("carol", "Carol", "Security approved"));
76+
77+
Assert.Equal(MutationRequestStatus.Pending, afterCarol.Status);
78+
Assert.Contains(afterCarol.Decisions, decision =>
79+
decision.Type == MutationRequestDecisionType.Approval(MutationRequestApprovalDecisionType.QuorumSatisfied));
80+
81+
var securityGroup = afterCarol.ApprovalRequirements
82+
.Where(requirement => requirement.ApprovalGroupId == "security-quorum")
83+
.ToList();
84+
85+
Assert.Equal(2, securityGroup.Count(requirement => requirement.Status == MutationApprovalRequirementStatus.Approved));
86+
Assert.Equal(1, securityGroup.Count(requirement => requirement.Status == MutationApprovalRequirementStatus.Satisfied));
87+
88+
var financeApproval = afterCarol.ApprovalRequirements.Single(requirement => requirement.ApproverRole == "finance-approver");
89+
var afterFinance = await manager.ApproveRequirement(
90+
request.RequestId,
91+
financeApproval.ApprovalId,
92+
MutationRequestApprovalWorkflowTestSupport.CreateRoleContext("frank", "Frank", "Finance approved", "finance-approver"));
93+
94+
Assert.Equal(MutationRequestStatus.Approved, afterFinance.Status);
95+
Assert.Null(afterFinance.PendingReason);
96+
}
97+
98+
[Fact]
99+
public async Task ApproveRequirement_accepts_role_and_group_targeting()
100+
{
101+
var store = new InMemoryMutationRequestStore();
102+
var manager = new MutationRequestApprovalWorkflowManager(store);
103+
var request = await store.Create(MutationRequestApprovalWorkflowTestSupport.CreateRoleAndGroupApprovalRequest());
104+
105+
var roleApproval = request.ApprovalRequirements.Single(requirement => requirement.ApproverRole == "security-admin");
106+
var afterRole = await manager.ApproveRequirement(
107+
request.RequestId,
108+
roleApproval.ApprovalId,
109+
MutationRequestApprovalWorkflowTestSupport.CreateRoleContext("sara", "Sara", "Security role approved", "security-admin"));
110+
111+
Assert.Equal(MutationApprovalRequirementStatus.Approved, afterRole.ApprovalRequirements.Single(requirement => requirement.ApprovalId == roleApproval.ApprovalId).Status);
112+
Assert.Equal(MutationRequestStatus.Pending, afterRole.Status);
113+
114+
var groupApproval = afterRole.ApprovalRequirements.Single(requirement => requirement.ApproverGroup == "ops-oncall");
115+
var afterGroup = await manager.ApproveRequirement(
116+
request.RequestId,
117+
groupApproval.ApprovalId,
118+
MutationRequestApprovalWorkflowTestSupport.CreateGroupContext("oliver", "Oliver", "Operations approved", "ops-oncall"));
119+
120+
Assert.Equal(MutationRequestStatus.Approved, afterGroup.Status);
121+
}
122+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using ModularityKit.Mutator.Abstractions.Context;
2+
using ModularityKit.Mutator.Governance.Abstractions.Approval.Model;
3+
using ModularityKit.Mutator.Governance.Abstractions.Exceptions.Approval;
4+
using ModularityKit.Mutator.Governance.Abstractions.Lifecycle.Model;
5+
using ModularityKit.Mutator.Governance.Abstractions.Requests.Decisions;
6+
using ModularityKit.Mutator.Governance.Runtime.Approval.Execution;
7+
using ModularityKit.Mutator.Governance.Runtime.Storage;
8+
using ModularityKit.Mutator.Governance.Tests.TestSupport.Approval.Workflow;
9+
using Xunit;
10+
11+
namespace ModularityKit.Mutator.Governance.Tests.Approval;
12+
13+
public sealed partial class MutationRequestApprovalWorkflowTests
14+
{
15+
[Fact]
16+
public async Task RejectRequirement_persists_structured_rejection_reason()
17+
{
18+
var store = new InMemoryMutationRequestStore();
19+
var manager = new MutationRequestApprovalWorkflowManager(store);
20+
var request = await store.Create(MutationRequestApprovalWorkflowTestSupport.CreateLinearApprovalRequest());
21+
var aliceApproval = request.ApprovalRequirements.Single(requirement => requirement.ApproverId == "alice");
22+
23+
var rejection = new MutationApprovalRejectionReason
24+
{
25+
Code = "missing-justification",
26+
Category = "policy",
27+
Message = "Change request did not include business justification.",
28+
Metadata = new Dictionary<string, object>
29+
{
30+
["TicketId"] = "CHG-42"
31+
}
32+
};
33+
34+
var rejected = await manager.RejectRequirement(
35+
request.RequestId,
36+
aliceApproval.ApprovalId,
37+
MutationContext.User("alice", "Alice", "Manager rejected"),
38+
rejection: rejection);
39+
40+
var rejectedRequirement = rejected.ApprovalRequirements.Single(requirement => requirement.ApprovalId == aliceApproval.ApprovalId);
41+
42+
Assert.Equal(MutationRequestStatus.Rejected, rejected.Status);
43+
Assert.Equal(MutationApprovalRequirementStatus.Rejected, rejectedRequirement.Status);
44+
Assert.NotNull(rejectedRequirement.Rejection);
45+
Assert.Equal("missing-justification", rejectedRequirement.Rejection!.Code);
46+
Assert.Contains(rejected.Decisions, decision =>
47+
decision.Type == MutationRequestDecisionType.Approval(MutationRequestApprovalDecisionType.Rejected) &&
48+
Equals(decision.Metadata["RejectionCode"], "missing-justification"));
49+
}
50+
51+
[Fact]
52+
public async Task ExpirePendingApprovals_rejects_requests_with_expired_approval_requirements()
53+
{
54+
var store = new InMemoryMutationRequestStore();
55+
var manager = new MutationRequestApprovalWorkflowManager(store);
56+
var request = await store.Create(MutationRequestApprovalWorkflowTestSupport.CreateExpiredApprovalRequest());
57+
58+
var expired = await manager.ExpirePendingApprovals(
59+
DateTimeOffset.UtcNow,
60+
MutationContext.Service("approval-timeout-monitor", "Expire stale approvals"));
61+
62+
var expiredRequest = Assert.Single(expired);
63+
64+
Assert.Equal(request.RequestId, expiredRequest.RequestId);
65+
Assert.Equal(MutationRequestStatus.Rejected, expiredRequest.Status);
66+
Assert.Contains(expiredRequest.ApprovalRequirements, requirement => requirement.Status == MutationApprovalRequirementStatus.Expired);
67+
Assert.Contains(expiredRequest.Decisions, decision =>
68+
decision.Type == MutationRequestDecisionType.Approval(MutationRequestApprovalDecisionType.Expired));
69+
}
70+
71+
[Fact]
72+
public async Task ApproveRequirement_throws_domain_exception_when_requirement_is_expired()
73+
{
74+
var store = new InMemoryMutationRequestStore();
75+
var manager = new MutationRequestApprovalWorkflowManager(store);
76+
var request = await store.Create(MutationRequestApprovalWorkflowTestSupport.CreateExpiredApprovalRequest());
77+
var expiredApproval = request.ApprovalRequirements.Single();
78+
79+
var exception = await Assert.ThrowsAsync<MutationApprovalRequirementExpiredException>(() =>
80+
manager.ApproveRequirement(
81+
request.RequestId,
82+
expiredApproval.ApprovalId,
83+
MutationContext.User("alice", "Alice", "Approve expired requirement")));
84+
85+
Assert.Equal(request.RequestId, exception.RequestId);
86+
Assert.Equal(expiredApproval.ApprovalId, exception.ApprovalId);
87+
}
88+
}

0 commit comments

Comments
 (0)