using EntitlementBLL.Legal; using EntitlementDAL.DTOs; using EntitlementDAL.Interfaces; using GB5Shared.DTO.Framework.AutoNumber; using GB5Shared.DTO.Framework.CommonConfig; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using Microsoft.Extensions.Options; using Moq; using System.Data.Common; using Xunit; namespace EntitlementTests; /// /// Covers the P0 Legal/Contract Agreement Consent integration link (tracker §50/§51): /// AgreementAuthoringBLL's Draft/Published/Superseded lifecycle guards, and /// LocalAgreementConsentProvider's bundle resolution + append-only, re-validated acceptance /// recording. /// public class AgreementConsentTests { private static AutoNumber BuildAutoNumber(long nextId) { var queryExecutor = new Mock(); queryExecutor .Setup(q => q.QueryAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List { nextId }); var dbConfig = new Mock>(); dbConfig.Setup(o => o.Value).Returns(new Gb5SystemDTO { DataBaseType = 0 }); return new AutoNumber(queryExecutor.Object, dbConfig.Object); } private static LoginDTO Login() => new() { ClientId = -1, UserId = -1 }; // ── AgreementAuthoringBLL ──────────────────────────────────────────────── [Fact] public async Task EnsureTypeAsync_TypeAlreadyExists_ReturnsExisting_NoInsert() { var dal = new Mock(); var existing = new AgreementTypeDTO { AgreementTypeId = 1, TypeCode = "TOS", TypeName = "Terms of Service" }; dal.Setup(d => d.GetTypeByCodeAsync("TOS", It.IsAny(), It.IsAny())).ReturnsAsync(existing); var bll = new AgreementAuthoringBLL(dal.Object, BuildAutoNumber(9001)); var result = await bll.EnsureTypeAsync("TOS", "Terms of Service", false, Login(), CancellationToken.None); Assert.Same(existing, result); dal.Verify(d => d.InsertTypeAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task EnsureTypeAsync_NotFound_CreatesNewType() { var dal = new Mock(); dal.Setup(d => d.GetTypeByCodeAsync("EULA", It.IsAny(), It.IsAny())).ReturnsAsync((AgreementTypeDTO?)null); var bll = new AgreementAuthoringBLL(dal.Object, BuildAutoNumber(9001)); var result = await bll.EnsureTypeAsync("EULA", "End User License Agreement", true, Login(), CancellationToken.None); Assert.Equal(9000, result.AgreementTypeId); Assert.True(result.RequiresIndividualAcceptance); dal.Verify(d => d.InsertTypeAsync( It.Is(t => t.TypeCode == "EULA" && t.AgreementTypeId == 9000), It.IsAny(), It.IsAny()), Times.Once); } [Theory] [InlineData("", "Name")] [InlineData("CODE", "")] public async Task EnsureTypeAsync_MissingRequiredField_Throws(string code, string name) { var dal = new Mock(); var bll = new AgreementAuthoringBLL(dal.Object, BuildAutoNumber(1)); await Assert.ThrowsAsync(() => bll.EnsureTypeAsync(code, name, false, Login(), CancellationToken.None)); } [Fact] public async Task CreateDraftVersionAsync_TypeDoesNotExist_Throws() { var dal = new Mock(); dal.Setup(d => d.GetTypeByCodeAsync("TOS", It.IsAny(), It.IsAny())).ReturnsAsync((AgreementTypeDTO?)null); var bll = new AgreementAuthoringBLL(dal.Object, BuildAutoNumber(1)); await Assert.ThrowsAsync(() => bll.CreateDraftVersionAsync("TOS", "v1.0", null, "https://example.com/tos-v1", DateTime.UtcNow, Login(), CancellationToken.None)); } [Fact] public async Task CreateDraftVersionAsync_HappyPath_CreatesDraft() { var dal = new Mock(); dal.Setup(d => d.GetTypeByCodeAsync("TOS", It.IsAny(), It.IsAny())) .ReturnsAsync(new AgreementTypeDTO { AgreementTypeId = 5, TypeCode = "TOS" }); var bll = new AgreementAuthoringBLL(dal.Object, BuildAutoNumber(9002)); var result = await bll.CreateDraftVersionAsync("TOS", "v1.0", "IN", "https://example.com/tos-v1", DateTime.UtcNow, Login(), CancellationToken.None); Assert.Equal(9001, result.AgreementVersionId); Assert.Equal((byte)AgreementVersionStatus.Draft, result.VersionStatus); dal.Verify(d => d.InsertVersionAsync( It.Is(v => v.AgreementTypeId == 5 && v.JurisdictionCode == "IN" && v.VersionStatus == (byte)AgreementVersionStatus.Draft), It.IsAny(), It.IsAny()), Times.Once); } [Fact] public async Task PublishVersionAsync_NotFound_Throws() { var dal = new Mock(); dal.Setup(d => d.GetVersionByIdAsync(999, It.IsAny(), It.IsAny())).ReturnsAsync((AgreementVersionDTO?)null); var bll = new AgreementAuthoringBLL(dal.Object, BuildAutoNumber(1)); await Assert.ThrowsAsync(() => bll.PublishVersionAsync(999, Login(), CancellationToken.None)); } [Fact] public async Task PublishVersionAsync_NotDraft_Throws() { var dal = new Mock(); dal.Setup(d => d.GetVersionByIdAsync(1, It.IsAny(), It.IsAny())) .ReturnsAsync(new AgreementVersionDTO { AgreementVersionId = 1, VersionStatus = (byte)AgreementVersionStatus.Published }); var bll = new AgreementAuthoringBLL(dal.Object, BuildAutoNumber(1)); await Assert.ThrowsAsync(() => bll.PublishVersionAsync(1, Login(), CancellationToken.None)); dal.Verify(d => d.PublishVersionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task PublishVersionAsync_Draft_PublishesWithComputedHash() { var dal = new Mock(); dal.Setup(d => d.GetVersionByIdAsync(1, It.IsAny(), It.IsAny())) .ReturnsAsync(new AgreementVersionDTO { AgreementVersionId = 1, VersionStatus = (byte)AgreementVersionStatus.Draft, ContentRef = "https://example.com/tos-v1" }); var bll = new AgreementAuthoringBLL(dal.Object, BuildAutoNumber(1)); await bll.PublishVersionAsync(1, Login(), CancellationToken.None); dal.Verify(d => d.PublishVersionAsync(1, It.Is(h => !string.IsNullOrEmpty(h)), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } [Fact] public async Task SupersedeVersionAsync_NotPublished_Throws() { var dal = new Mock(); dal.Setup(d => d.GetVersionByIdAsync(1, It.IsAny(), It.IsAny())) .ReturnsAsync(new AgreementVersionDTO { AgreementVersionId = 1, VersionStatus = (byte)AgreementVersionStatus.Draft }); var bll = new AgreementAuthoringBLL(dal.Object, BuildAutoNumber(1)); await Assert.ThrowsAsync(() => bll.SupersedeVersionAsync(1, Login(), CancellationToken.None)); } [Fact] public async Task SupersedeVersionAsync_Published_Supersedes() { var dal = new Mock(); dal.Setup(d => d.GetVersionByIdAsync(1, It.IsAny(), It.IsAny())) .ReturnsAsync(new AgreementVersionDTO { AgreementVersionId = 1, VersionStatus = (byte)AgreementVersionStatus.Published }); var bll = new AgreementAuthoringBLL(dal.Object, BuildAutoNumber(1)); await bll.SupersedeVersionAsync(1, Login(), CancellationToken.None); dal.Verify(d => d.UpdateVersionStatusAsync(1, (byte)AgreementVersionStatus.Superseded, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } // ── LocalAgreementConsentProvider ──────────────────────────────────────── [Fact] public async Task GetApplicableAgreementsAsync_NoTypeCodes_Throws() { var dal = new Mock(); var provider = new LocalAgreementConsentProvider(dal.Object, BuildAutoNumber(1)); await Assert.ThrowsAsync(() => provider.GetApplicableAgreementsAsync("IN", Array.Empty(), Login(), CancellationToken.None)); } [Fact] public async Task GetApplicableAgreementsAsync_HappyPath_ReturnsBundle() { var dal = new Mock(); var versions = new List { new() { AgreementVersionId = 1 }, new() { AgreementVersionId = 2 } }; dal.Setup(d => d.GetApplicableVersionsAsync( It.Is>(c => c.Contains("TOS") && c.Contains("AUP")), "IN", It.IsAny(), It.IsAny())) .ReturnsAsync(versions); var provider = new LocalAgreementConsentProvider(dal.Object, BuildAutoNumber(1)); var bundle = await provider.GetApplicableAgreementsAsync("IN", new[] { "TOS", "AUP" }, Login(), CancellationToken.None); Assert.Equal(2, bundle.Versions.Count); } [Fact] public async Task RecordAcceptanceAsync_NoVersionIds_Throws() { var dal = new Mock(); var provider = new LocalAgreementConsentProvider(dal.Object, BuildAutoNumber(1)); await Assert.ThrowsAsync(() => provider.RecordAcceptanceAsync(AgreementSubjectType.Prospect, 1, Array.Empty(), "1.2.3.4", "UA", AgreementAcceptanceMethod.Checkbox, "corr-1", Login(), CancellationToken.None)); } [Fact] public async Task RecordAcceptanceAsync_EmptyCorrelationKey_Throws() { var dal = new Mock(); var provider = new LocalAgreementConsentProvider(dal.Object, BuildAutoNumber(1)); await Assert.ThrowsAsync(() => provider.RecordAcceptanceAsync(AgreementSubjectType.Prospect, 1, new[] { 10 }, "1.2.3.4", "UA", AgreementAcceptanceMethod.Checkbox, "", Login(), CancellationToken.None)); } [Fact] public async Task RecordAcceptanceAsync_VersionNoLongerPublished_ThrowsAndInsertsNothing() { var dal = new Mock(); dal.Setup(d => d.GetVersionByIdAsync(10, It.IsAny(), It.IsAny())) .ReturnsAsync(new AgreementVersionDTO { AgreementVersionId = 10, VersionStatus = (byte)AgreementVersionStatus.Superseded }); var provider = new LocalAgreementConsentProvider(dal.Object, BuildAutoNumber(1)); await Assert.ThrowsAsync(() => provider.RecordAcceptanceAsync(AgreementSubjectType.Prospect, 1, new[] { 10 }, "1.2.3.4", "UA", AgreementAcceptanceMethod.Checkbox, "corr-1", Login(), CancellationToken.None)); dal.Verify(d => d.InsertAcceptanceAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task RecordAcceptanceAsync_HappyPath_InsertsOneRowPerVersion_SameCorrelationKey() { var dal = new Mock(); dal.Setup(d => d.GetVersionByIdAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((int id, LoginDTO _, CancellationToken _) => new AgreementVersionDTO { AgreementVersionId = id, VersionStatus = (byte)AgreementVersionStatus.Published }); var inserted = new List(); dal.Setup(d => d.InsertAcceptanceAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((dto, _, _) => inserted.Add(dto)) .Returns(Task.CompletedTask); var provider = new LocalAgreementConsentProvider(dal.Object, BuildAutoNumber(9003)); await provider.RecordAcceptanceAsync(AgreementSubjectType.Prospect, 42, new[] { 10, 11 }, "1.2.3.4", "UA", AgreementAcceptanceMethod.Clickwrap, "corr-1", Login(), CancellationToken.None); Assert.Equal(2, inserted.Count); Assert.All(inserted, a => Assert.Equal("corr-1", a.CorrelationKey)); Assert.All(inserted, a => Assert.Equal(42, a.SubjectId)); Assert.All(inserted, a => Assert.Equal((byte)AgreementSubjectType.Prospect, a.SubjectType)); Assert.All(inserted, a => Assert.Equal((byte)AgreementAcceptanceMethod.Clickwrap, a.AcceptanceMethod)); Assert.Contains(inserted, a => a.AgreementVersionId == 10); Assert.Contains(inserted, a => a.AgreementVersionId == 11); } }