using GB5Shared.DTO.Framework.Login; using GB5Shared.QueryExecutor; using Moq; using SwBLL.ClientDatabase; using SwBLL.DbServer; using SwBLL.DdlScript; using SwBLL.Provisioning; using SwBLL.UpgradePackage; using SwBLL.UserResourceRole; using SwDAL.DTO.ClientDatabase; using SwDAL.DTO.Provisioning; using SwDAL.DTO.UpgradePackage; using SwDAL.Enums; using SwDAL.Query.Provisioning; using Xunit; namespace SwTests; /// /// Covers ProvisioningBLL.GetCurrentVersion — resolves the currently-applied release version /// for a ClientDatabaseId by finding the most recently fully-applied package (via /// IQueryExecutor + ProvisioningLogQB directly, matching this class's own "no foreign DALs" /// architecture rule — same pattern LogProvisioningAsync already uses for this table's writes), /// then walking SupersedesPackageId back through the DbModel's lineage (IUpgradePackageBLL.GetLineage) /// to the nearest package carrying a non-null ReleaseVersion. "Unversioned" (no ReleaseVersion /// ever reached) is a legitimate result, not an exception — most of these tests assert that. /// /// IDdlScriptBLL/IDbServerBLL/ITargetDbExecutor/IUserResourceRoleBLL are passed null! — /// GetCurrentVersion never touches them (only ProvisionClientDatabase does), same "safe because /// unused" convention as UpgradePackageBLLTests. /// public class ProvisioningBLLTests { private static (ProvisioningBLL Svc, Mock ClientDatabaseBLL, Mock UpgradePackageBLL, Mock QueryExecutor) BuildService() { var clientDatabaseBLL = new Mock(); var upgradePackageBLL = new Mock(); var queryExecutor = new Mock(); var svc = new ProvisioningBLL( clientDatabaseBLL.Object, ddlScriptBLL: null!, upgradePackageBLL.Object, dbServerBLL: null!, targetDbExecutor: null!, userResourceRoleBLL: null!, queryExecutor.Object); return (svc, clientDatabaseBLL, upgradePackageBLL, queryExecutor); } private static readonly LoginDTO Login = new() { ClientId = 42, UserId = 1 }; private static void SetupMostRecentlyApplied(Mock queryExecutor, AppliedPackageDTO? result) => queryExecutor.Setup(q => q.QuerySingleAsync( It.IsAny(), ProvisioningLogQB.GET_MOST_RECENTLY_APPLIED_PACKAGE, It.IsAny(), null, false, It.IsAny())) .ReturnsAsync(result!); [Fact] public async Task GetCurrentVersion_ZeroClientDbId_ThrowsWithoutTouchingDeps() { var (svc, clientDatabaseBLL, upgradePackageBLL, queryExecutor) = BuildService(); await Assert.ThrowsAsync(() => svc.GetCurrentVersion(0, Login, default)); clientDatabaseBLL.Verify(c => c.GetById(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); upgradePackageBLL.Verify(u => u.GetLineage(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task GetCurrentVersion_ClientDbNotFound_Throws() { var (svc, clientDatabaseBLL, _, _) = BuildService(); clientDatabaseBLL.Setup(c => c.GetById(5, Login, default)).ReturnsAsync((ClientDatabaseDTO?)null); await Assert.ThrowsAsync(() => svc.GetCurrentVersion(5, Login, default)); } [Fact] public async Task GetCurrentVersion_NothingEverApplied_ReturnsUnversionedNotError() { var (svc, clientDatabaseBLL, _, queryExecutor) = BuildService(); clientDatabaseBLL.Setup(c => c.GetById(5, Login, default)).ReturnsAsync(new ClientDatabaseDTO { ClientDbId = 5, DbModelId = 7 }); SetupMostRecentlyApplied(queryExecutor, null); var result = await svc.GetCurrentVersion(5, Login, default); Assert.False(result.IsVersioned); Assert.Null(result.CurrentReleaseVersion); Assert.Null(result.MostRecentlyAppliedPackageId); } [Fact] public async Task GetCurrentVersion_MostRecentPackageItselfVersioned_ReturnsItsVersionDirectly() { var (svc, clientDatabaseBLL, upgradePackageBLL, queryExecutor) = BuildService(); clientDatabaseBLL.Setup(c => c.GetById(5, Login, default)).ReturnsAsync(new ClientDatabaseDTO { ClientDbId = 5, DbModelId = 7 }); SetupMostRecentlyApplied(queryExecutor, new AppliedPackageDTO { PackageId = 100, ReleaseVersion = "4.2.0", DbModelId = 7 }); upgradePackageBLL.Setup(u => u.GetLineage(7, Login, default)).ReturnsAsync(new[] { new LineageNodeDTO { PackageId = 100, ReleaseVersion = "4.2.0", SupersedesPackageId = null }, }); var result = await svc.GetCurrentVersion(5, Login, default); Assert.True(result.IsVersioned); Assert.Equal("4.2.0", result.CurrentReleaseVersion); Assert.Equal(100, result.ResolvedFromPackageId); Assert.Equal(100, result.MostRecentlyAppliedPackageId); } [Fact] public async Task GetCurrentVersion_MostRecentPackageUnversioned_WalksToNearestVersionedAncestor() { var (svc, clientDatabaseBLL, upgradePackageBLL, queryExecutor) = BuildService(); clientDatabaseBLL.Setup(c => c.GetById(5, Login, default)).ReturnsAsync(new ClientDatabaseDTO { ClientDbId = 5, DbModelId = 7 }); // Most recently applied is a client feature-bundle (unversioned) that supersedes an // intermediate unversioned hotfix, which in turn supersedes the release boundary. SetupMostRecentlyApplied(queryExecutor, new AppliedPackageDTO { PackageId = 102, ReleaseVersion = null, DbModelId = 7 }); upgradePackageBLL.Setup(u => u.GetLineage(7, Login, default)).ReturnsAsync(new[] { new LineageNodeDTO { PackageId = 100, ReleaseVersion = "4.2.0", SupersedesPackageId = null }, new LineageNodeDTO { PackageId = 101, ReleaseVersion = null, SupersedesPackageId = 100 }, new LineageNodeDTO { PackageId = 102, ReleaseVersion = null, SupersedesPackageId = 101 }, }); var result = await svc.GetCurrentVersion(5, Login, default); Assert.True(result.IsVersioned); Assert.Equal("4.2.0", result.CurrentReleaseVersion); Assert.Equal(100, result.ResolvedFromPackageId); Assert.Equal(102, result.MostRecentlyAppliedPackageId); } [Fact] public async Task GetCurrentVersion_ChainRootReachedWithNoVersion_ReturnsUnversioned() { var (svc, clientDatabaseBLL, upgradePackageBLL, queryExecutor) = BuildService(); clientDatabaseBLL.Setup(c => c.GetById(5, Login, default)).ReturnsAsync(new ClientDatabaseDTO { ClientDbId = 5, DbModelId = 7 }); SetupMostRecentlyApplied(queryExecutor, new AppliedPackageDTO { PackageId = 101, ReleaseVersion = null, DbModelId = 7 }); upgradePackageBLL.Setup(u => u.GetLineage(7, Login, default)).ReturnsAsync(new[] { new LineageNodeDTO { PackageId = 100, ReleaseVersion = null, SupersedesPackageId = null }, new LineageNodeDTO { PackageId = 101, ReleaseVersion = null, SupersedesPackageId = 100 }, }); var result = await svc.GetCurrentVersion(5, Login, default); Assert.False(result.IsVersioned); Assert.Null(result.CurrentReleaseVersion); Assert.Equal(101, result.MostRecentlyAppliedPackageId); } [Fact] public async Task GetCurrentVersion_MissingLineageNode_StopsWalkGracefully() { var (svc, clientDatabaseBLL, upgradePackageBLL, queryExecutor) = BuildService(); clientDatabaseBLL.Setup(c => c.GetById(5, Login, default)).ReturnsAsync(new ClientDatabaseDTO { ClientDbId = 5, DbModelId = 7 }); // The most-recently-applied package itself isn't present in the fetched lineage — // a data-integrity edge case (e.g. cross-DbModel drift) — must not throw. SetupMostRecentlyApplied(queryExecutor, new AppliedPackageDTO { PackageId = 999, ReleaseVersion = null, DbModelId = 7 }); upgradePackageBLL.Setup(u => u.GetLineage(7, Login, default)).ReturnsAsync(new[] { new LineageNodeDTO { PackageId = 100, ReleaseVersion = "4.2.0", SupersedesPackageId = null }, }); var result = await svc.GetCurrentVersion(5, Login, default); Assert.False(result.IsVersioned); Assert.Null(result.CurrentReleaseVersion); } [Fact] public async Task GetCurrentVersion_CyclicSupersedesChain_TerminatesViaVisitedGuard() { var (svc, clientDatabaseBLL, upgradePackageBLL, queryExecutor) = BuildService(); clientDatabaseBLL.Setup(c => c.GetById(5, Login, default)).ReturnsAsync(new ClientDatabaseDTO { ClientDbId = 5, DbModelId = 7 }); SetupMostRecentlyApplied(queryExecutor, new AppliedPackageDTO { PackageId = 100, ReleaseVersion = null, DbModelId = 7 }); // 100 -> 101 -> 100 -> ... a corrupt/cyclic SupersedesPackageId graph, neither ever versioned. upgradePackageBLL.Setup(u => u.GetLineage(7, Login, default)).ReturnsAsync(new[] { new LineageNodeDTO { PackageId = 100, ReleaseVersion = null, SupersedesPackageId = 101 }, new LineageNodeDTO { PackageId = 101, ReleaseVersion = null, SupersedesPackageId = 100 }, }); var result = await svc.GetCurrentVersion(5, Login, default); Assert.False(result.IsVersioned); Assert.Null(result.CurrentReleaseVersion); } // ── ProvisionClientDatabase — {{TENANTID}} substitution (tracker §35 Pillar 3) ────────── private static (ProvisioningBLL Svc, Mock ClientDatabaseBLL, Mock DdlScriptBLL, Mock UpgradePackageBLL, Mock DbServerBLL, Mock TargetDbExecutor, Mock UserResourceRoleBLL) BuildServiceForProvisioning() { var clientDatabaseBLL = new Mock(); var ddlScriptBLL = new Mock(); var upgradePackageBLL = new Mock(); var dbServerBLL = new Mock(); var targetDbExecutor = new Mock(); var userResourceRoleBLL = new Mock(); var queryExecutor = new Mock(); var svc = new ProvisioningBLL( clientDatabaseBLL.Object, ddlScriptBLL.Object, upgradePackageBLL.Object, dbServerBLL.Object, targetDbExecutor.Object, userResourceRoleBLL.Object, queryExecutor.Object); return (svc, clientDatabaseBLL, ddlScriptBLL, upgradePackageBLL, dbServerBLL, targetDbExecutor, userResourceRoleBLL); } [Fact] public async Task ProvisionClientDatabase_ScriptContainsTenantIdToken_SubstitutesRealTenantIdBeforeExecuting() { var (svc, clientDatabaseBLL, ddlScriptBLL, upgradePackageBLL, dbServerBLL, targetDbExecutor, _) = BuildServiceForProvisioning(); clientDatabaseBLL.Setup(c => c.GetById(5, Login, default)) .ReturnsAsync(new ClientDatabaseDTO { ClientDbId = 5, DbServerId = 1, DatabaseName = "TestDb", TenantId = -1399999706 }); dbServerBLL.Setup(d => d.GetByIdWithCredentials(1, Login, default)) .ReturnsAsync(new SwDAL.DTO.DbServer.DbServerDTO { DbServerId = 1 }); targetDbExecutor.Setup(t => t.BuildConnectionString(It.IsAny(), "TestDb", null, null)) .Returns("fake-conn-str"); upgradePackageBLL.Setup(u => u.GetDdlLines(10, Login, default)) .ReturnsAsync(new[] { new UpgradePackageDdlDTO { PackageDdlId = 1, PackageId = 10, DdlScriptId = 200, Sequence = 1 } }); ddlScriptBLL.Setup(d => d.GetById(200, Login, default)) .ReturnsAsync(new SwDAL.DTO.DdlScript.DdlScriptDTO { DdlScriptId = 200, ScriptStatus = (byte)DdlScriptStatus.Approved, SqlScript = "INSERT INTO MCURRENCY (TENANTID) VALUES ({{TENANTID}})", }); string? capturedSql = null; targetDbExecutor.Setup(t => t.ExecuteScriptAsync("fake-conn-str", It.IsAny(), default)) .Callback((_, sql, _) => capturedSql = sql) .ReturnsAsync(1); await svc.ProvisionClientDatabase(5, 10, Login, default); Assert.Equal("INSERT INTO MCURRENCY (TENANTID) VALUES (-1399999706)", capturedSql); } // ── ProvisionClientDatabase — per-user role-grant gate (SW.MSWUSERRESOURCEROLE) ──────── // Provisioning always uses the flat DbServer admin connection (contained users don't exist // yet) — the gate is one level up, fail-closed ONLY when an explicit, insufficient grant is // on record; no grant configured at all is fail-open, same posture as the module's other // enforcement call sites. private static void SetupMinimalProvisioning( Mock clientDatabaseBLL, Mock dbServerBLL, Mock upgradePackageBLL, Mock ddlScriptBLL, Mock targetDbExecutor) { clientDatabaseBLL.Setup(c => c.GetById(5, Login, default)) .ReturnsAsync(new ClientDatabaseDTO { ClientDbId = 5, DbServerId = 1, DatabaseName = "TestDb" }); dbServerBLL.Setup(d => d.GetByIdWithCredentials(1, Login, default)) .ReturnsAsync(new SwDAL.DTO.DbServer.DbServerDTO { DbServerId = 1 }); targetDbExecutor.Setup(t => t.BuildConnectionString(It.IsAny(), "TestDb", null, null)) .Returns("fake-conn-str"); upgradePackageBLL.Setup(u => u.GetDdlLines(10, Login, default)) .ReturnsAsync(new[] { new UpgradePackageDdlDTO { PackageDdlId = 1, PackageId = 10, DdlScriptId = 200, Sequence = 1 } }); ddlScriptBLL.Setup(d => d.GetById(200, Login, default)) .ReturnsAsync(new SwDAL.DTO.DdlScript.DdlScriptDTO { DdlScriptId = 200, ScriptStatus = (byte)DdlScriptStatus.Approved, SqlScript = "SELECT 1", }); targetDbExecutor.Setup(t => t.ExecuteScriptAsync("fake-conn-str", It.IsAny(), default)) .ReturnsAsync(1); } [Fact] public async Task ProvisionClientDatabase_NoGrantConfigured_FailsOpen_Proceeds() { var (svc, clientDatabaseBLL, ddlScriptBLL, upgradePackageBLL, dbServerBLL, targetDbExecutor, userResourceRoleBLL) = BuildServiceForProvisioning(); SetupMinimalProvisioning(clientDatabaseBLL, dbServerBLL, upgradePackageBLL, ddlScriptBLL, targetDbExecutor); userResourceRoleBLL.Setup(u => u.GetEffectiveRole(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((ClientDbLoginRole?)null); await svc.ProvisionClientDatabase(5, 10, Login, default); targetDbExecutor.Verify(t => t.ExecuteScriptAsync("fake-conn-str", It.IsAny(), default), Times.Once); } [Fact] public async Task ProvisionClientDatabase_DbaGrant_Proceeds() { var (svc, clientDatabaseBLL, ddlScriptBLL, upgradePackageBLL, dbServerBLL, targetDbExecutor, userResourceRoleBLL) = BuildServiceForProvisioning(); SetupMinimalProvisioning(clientDatabaseBLL, dbServerBLL, upgradePackageBLL, ddlScriptBLL, targetDbExecutor); userResourceRoleBLL.Setup(u => u.GetEffectiveRole(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((ClientDbLoginRole?)ClientDbLoginRole.Dba); await svc.ProvisionClientDatabase(5, 10, Login, default); targetDbExecutor.Verify(t => t.ExecuteScriptAsync("fake-conn-str", It.IsAny(), default), Times.Once); } [Fact] public async Task ProvisionClientDatabase_InsufficientExplicitGrant_ThrowsWithoutExecuting() { var (svc, clientDatabaseBLL, ddlScriptBLL, upgradePackageBLL, dbServerBLL, targetDbExecutor, userResourceRoleBLL) = BuildServiceForProvisioning(); clientDatabaseBLL.Setup(c => c.GetById(5, Login, default)) .ReturnsAsync(new ClientDatabaseDTO { ClientDbId = 5, DbServerId = 1, DatabaseName = "TestDb" }); dbServerBLL.Setup(d => d.GetByIdWithCredentials(1, Login, default)) .ReturnsAsync(new SwDAL.DTO.DbServer.DbServerDTO { DbServerId = 1 }); userResourceRoleBLL.Setup(u => u.GetEffectiveRole(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((ClientDbLoginRole?)ClientDbLoginRole.ReadOnly); await Assert.ThrowsAsync(() => svc.ProvisionClientDatabase(5, 10, Login, default)); targetDbExecutor.Verify(t => t.ExecuteScriptAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } }