using BigRegister.Api.Contracts; using BigRegister.Api.Data; using BigRegister.Domain.Authorization; using BigRegister.Domain.Letters; namespace BigRegister.Tests.Domain; public class BriefRuleTests { private static BriefStatusDto Status(string tag) => new(tag); private static readonly Principal Drafter = new(PrincipalRole.Drafter); private static readonly Principal Approver = new(PrincipalRole.Approver); // --- CanSave ----------------------------------------------------------------- [Theory] [InlineData("draft")] [InlineData("rejected")] public void A_drafter_may_save_a_draft_or_rejected_letter(string tag) => Assert.Equal(BriefStore.Outcome.Ok, BriefRules.CanSave(Status(tag), isDrafter: true)); [Theory] [InlineData("submitted")] [InlineData("approved")] [InlineData("sent")] public void A_drafter_may_not_save_a_non_editable_letter(string tag) => Assert.Equal(BriefStore.Outcome.Conflict, BriefRules.CanSave(Status(tag), isDrafter: true)); [Theory] [InlineData("draft")] [InlineData("submitted")] public void A_non_drafter_is_forbidden_to_save_regardless_of_status(string tag) => // Role is checked before status: Forbidden wins even against an otherwise-open status. Assert.Equal(BriefStore.Outcome.Forbidden, BriefRules.CanSave(Status(tag), isDrafter: false)); // --- StatusAfterSave ----------------------------------------------------------- [Fact] public void Saving_a_rejected_letter_reopens_it_to_draft() => Assert.Equal("draft", BriefRules.StatusAfterSave(Status("rejected")).Tag); [Fact] public void Saving_a_draft_letter_leaves_its_status_unchanged() => Assert.Equal("draft", BriefRules.StatusAfterSave(Status("draft")).Tag); // --- RequiredFilled -------------------------------------------------------------- private static LetterSectionDto Section(string key, bool required, int blockCount) => new(key, key, required, Enumerable.Range(0, blockCount) .Select(i => new LetterBlockDto("freeText", $"{key}-{i}", new RichTextBlockDto(Array.Empty()))) .ToList()); [Fact] public void No_required_sections_means_nothing_to_fill() => Assert.True(BriefRules.RequiredFilled(Array.Empty())); [Fact] public void An_optional_empty_section_does_not_block_submission() => Assert.True(BriefRules.RequiredFilled(new[] { Section("slot", required: false, blockCount: 0) })); [Fact] public void A_required_section_with_a_block_is_filled() => Assert.True(BriefRules.RequiredFilled(new[] { Section("kern", required: true, blockCount: 1) })); [Fact] public void A_required_section_with_no_blocks_is_not_filled() => Assert.False(BriefRules.RequiredFilled(new[] { Section("kern", required: true, blockCount: 0) })); [Fact] public void One_unfilled_required_section_blocks_submission_even_if_others_are_filled() => Assert.False(BriefRules.RequiredFilled(new[] { Section("kern", required: true, blockCount: 1), Section("bijlage", required: true, blockCount: 0), })); // --- CanSubmit ----------------------------------------------------------------- [Fact] public void A_drafter_may_submit_a_filled_draft() => Assert.Equal(BriefStore.Outcome.Ok, BriefRules.CanSubmit(Status("draft"), isDrafter: true, requiredFilled: true)); [Fact] public void A_drafter_may_not_submit_an_unfilled_draft() => Assert.Equal(BriefStore.Outcome.Conflict, BriefRules.CanSubmit(Status("draft"), isDrafter: true, requiredFilled: false)); [Fact] public void A_drafter_may_not_submit_a_letter_that_is_not_a_draft() => Assert.Equal(BriefStore.Outcome.Conflict, BriefRules.CanSubmit(Status("submitted"), isDrafter: true, requiredFilled: true)); [Fact] public void A_non_drafter_is_forbidden_to_submit_even_a_filled_draft() => // Role is checked before status/completeness: Forbidden wins over Conflict. Assert.Equal(BriefStore.Outcome.Forbidden, BriefRules.CanSubmit(Status("draft"), isDrafter: false, requiredFilled: true)); // --- CanSend --------------------------------------------------------------------- [Fact] public void An_approved_letter_may_be_sent() => Assert.Equal(BriefStore.Outcome.Ok, BriefRules.CanSend(Status("approved"))); [Theory] [InlineData("draft")] [InlineData("submitted")] [InlineData("rejected")] [InlineData("sent")] public void Only_an_approved_letter_may_be_sent(string tag) => Assert.Equal(BriefStore.Outcome.Conflict, BriefRules.CanSend(Status(tag))); // --- CanDecide (Approve/Reject shared guard) -------------------------------------- [Theory] [InlineData(BriefAction.Approve)] [InlineData(BriefAction.Reject)] public void An_approver_may_decide_a_submitted_letter_drafted_by_someone_else(BriefAction action) => Assert.Equal( BriefStore.Outcome.Ok, BriefRules.CanDecide(action, Status("submitted"), Approver, drafterId: BriefStore.DrafterId)); [Fact] public void A_drafter_may_not_approve_or_reject() => Assert.Equal( BriefStore.Outcome.Forbidden, BriefRules.CanDecide(BriefAction.Approve, Status("submitted"), Drafter, drafterId: BriefStore.DrafterId)); [Fact] public void An_approver_may_not_decide_a_letter_they_drafted_themselves() => // Four-eyes / SoD: the acting approver id happens to equal the letter's drafterId. Assert.Equal( BriefStore.Outcome.Forbidden, BriefRules.CanDecide(BriefAction.Approve, Status("submitted"), Approver, drafterId: BriefStore.ApproverId)); [Fact] public void An_approver_may_not_decide_a_letter_that_is_not_submitted() => Assert.Equal( BriefStore.Outcome.Conflict, BriefRules.CanDecide(BriefAction.Approve, Status("draft"), Approver, drafterId: BriefStore.DrafterId)); [Fact] public void Entitlement_is_checked_before_status_forbidden_wins_over_conflict() => // Same actor as drafter AND a non-submitted status: still Forbidden, not Conflict — // matches the store's original check order (Authz.CanActOn before the status guard). Assert.Equal( BriefStore.Outcome.Forbidden, BriefRules.CanDecide(BriefAction.Approve, Status("draft"), Approver, drafterId: BriefStore.ApproverId)); }