using SwBLL.ServerConfig; using GB5Shared.DTO.Framework.Login; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using SwBLL.ClientDatabase; using SwBLL.Provisioning; using SwDAL.DTO.ClientDatabase; using SwDAL.DTO.DbServer; using VaultSharp; using Xunit; namespace SwTests; /// /// Covers ClientDatabaseProvisioner's guard clauses — the identifier/DbType/backup-filename /// validation that must reject bad input BEFORE any RESTORE/CREATE DATABASE statement is ever /// built, since ClientDbCode/DatabaseName are interpolated directly into DDL text (database/file /// names can't be SQL-parameterized). None of these tests need a real SQL Server or Vault — /// every case here is expected to throw before ITargetDbExecutor or IVaultClient is ever touched. /// public class ClientDatabaseProvisionerTests { private static (ClientDatabaseProvisioner Svc, Mock TargetDbExecutor) BuildService() { var targetDbExecutor = new Mock(); var provisioningBLL = new Mock(); var clientDatabaseBLL = new Mock(); var serverConfigBLL = new Mock(); // never touched by the guard tests below var vaultClient = new Mock(); // never touched by the guard tests below var options = Options.Create(new SqlServerProvisioningOptions { BackupSourcePath = "D:\\Backups", DataFileDestinationPath = "D:\\Data", LogFileDestinationPath = "D:\\Logs" }); var svc = new ClientDatabaseProvisioner( targetDbExecutor.Object, provisioningBLL.Object, clientDatabaseBLL.Object, serverConfigBLL.Object, vaultClient.Object, options, NullLogger.Instance); return (svc, targetDbExecutor); } private static ClientDatabaseDTO ValidClientDb(string dbCode = "acme01", string dbName = "acme01") => new() { ClientDbId = 1, ClientDbCode = dbCode, ClientDbName = "Acme Corp", DatabaseName = dbName, DbServerId = 1, DbModelId = 1, DatabaseRole = 0 }; private static DbServerDTO SqlServerServer() => new() { DbServerId = 1, DbType = 0, HostName = "sql01" }; private static DbServerDTO PostgresServer() => new() { DbServerId = 2, DbType = 1, HostName = "pg01" }; private static LoginDTO TestLogin() => new() { ClientId = 42, UserId = -1 }; [Theory] [InlineData("acme; DROP TABLE Users--")] [InlineData("1acme")] [InlineData("acme db")] [InlineData("")] public async Task Test_CreateFromScriptsAsync_InvalidDatabaseName_ThrowsBeforeAnyDbCall(string badName) { var (svc, targetDbExecutor) = BuildService(); var clientDb = ValidClientDb(dbName: badName); await Assert.ThrowsAsync(() => svc.CreateFromScriptsAsync(clientDb, SqlServerServer(), baselineUpgradePackageId: 1, TestLogin(), CancellationToken.None)); targetDbExecutor.Verify(t => t.BuildAdminConnectionString(It.IsAny()), Times.Never); } [Theory] [InlineData("acme; DROP TABLE Users--")] [InlineData("1acme")] public async Task Test_CreateFromScriptsAsync_InvalidClientDbCode_ThrowsBeforeAnyDbCall(string badCode) { var (svc, targetDbExecutor) = BuildService(); var clientDb = ValidClientDb(dbCode: badCode); await Assert.ThrowsAsync(() => svc.CreateFromScriptsAsync(clientDb, SqlServerServer(), baselineUpgradePackageId: 1, TestLogin(), CancellationToken.None)); targetDbExecutor.Verify(t => t.BuildAdminConnectionString(It.IsAny()), Times.Never); } [Fact] public async Task Test_CreateFromScriptsAsync_NonSqlServerTarget_ThrowsNotSupported() { var (svc, targetDbExecutor) = BuildService(); await Assert.ThrowsAsync(() => svc.CreateFromScriptsAsync(ValidClientDb(), PostgresServer(), baselineUpgradePackageId: 1, TestLogin(), CancellationToken.None)); targetDbExecutor.Verify(t => t.BuildAdminConnectionString(It.IsAny()), Times.Never); } [Fact] public async Task Test_CreateFromSnapshotAsync_NonSqlServerTarget_ThrowsNotSupported() { var (svc, targetDbExecutor) = BuildService(); await Assert.ThrowsAsync(() => svc.CreateFromSnapshotAsync(ValidClientDb(), PostgresServer(), "template.bak", TestLogin(), CancellationToken.None)); targetDbExecutor.Verify(t => t.BuildAdminConnectionString(It.IsAny()), Times.Never); } [Theory] [InlineData("../../etc/passwd")] [InlineData("template.bak; DROP TABLE Users--")] [InlineData("..\\..\\windows\\system32\\template.bak")] [InlineData("template.txt")] [InlineData("")] public async Task Test_CreateFromSnapshotAsync_InvalidTemplateBackupRef_ThrowsBeforeAnyDbCall(string badRef) { var (svc, targetDbExecutor) = BuildService(); await Assert.ThrowsAsync(() => svc.CreateFromSnapshotAsync(ValidClientDb(), SqlServerServer(), badRef, TestLogin(), CancellationToken.None)); targetDbExecutor.Verify(t => t.BuildAdminConnectionString(It.IsAny()), Times.Never); } [Fact] public async Task Test_CreateFromSnapshotAsync_ValidPlainFilename_PassesGuardsAndReachesTargetDbExecutor() { var (svc, targetDbExecutor) = BuildService(); targetDbExecutor.Setup(t => t.BuildAdminConnectionString(It.IsAny())).Returns("admin-conn-str"); targetDbExecutor.Setup(t => t.DatabaseExistsAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(false); // Guards all pass — it should proceed far enough to ask TargetDbExecutor for the admin // connection string (and then fail later trying to actually open it, which is expected // and fine here — this test only proves the guard clauses don't false-positive on valid input). await Assert.ThrowsAnyAsync(() => svc.CreateFromSnapshotAsync(ValidClientDb(), SqlServerServer(), "acme-template.bak", TestLogin(), CancellationToken.None)); targetDbExecutor.Verify(t => t.BuildAdminConnectionString(It.IsAny()), Times.Once); } // ── Existence check — RESTORE ... WITH REPLACE / CREATE DATABASE must never silently // overwrite or race a name collision with an existing database ────────────────────────── [Fact] public async Task Test_CreateFromSnapshotAsync_DatabaseAlreadyExists_ThrowsBeforeRestoring() { var (svc, targetDbExecutor) = BuildService(); targetDbExecutor.Setup(t => t.BuildAdminConnectionString(It.IsAny())).Returns("admin-conn-str"); targetDbExecutor.Setup(t => t.DatabaseExistsAsync("admin-conn-str", "acme01", It.IsAny())) .ReturnsAsync(true); await Assert.ThrowsAsync(() => svc.CreateFromSnapshotAsync(ValidClientDb(), SqlServerServer(), "acme-template.bak", TestLogin(), CancellationToken.None)); targetDbExecutor.Verify(t => t.DatabaseExistsAsync("admin-conn-str", "acme01", It.IsAny()), Times.Once); } [Fact] public async Task Test_CreateFromScriptsAsync_DatabaseAlreadyExists_ThrowsBeforeCreating() { var (svc, targetDbExecutor) = BuildService(); targetDbExecutor.Setup(t => t.BuildAdminConnectionString(It.IsAny())).Returns("admin-conn-str"); targetDbExecutor.Setup(t => t.DatabaseExistsAsync("admin-conn-str", "acme01", It.IsAny())) .ReturnsAsync(true); await Assert.ThrowsAsync(() => svc.CreateFromScriptsAsync(ValidClientDb(), SqlServerServer(), baselineUpgradePackageId: 1, TestLogin(), CancellationToken.None)); targetDbExecutor.Verify(t => t.DatabaseExistsAsync("admin-conn-str", "acme01", It.IsAny()), Times.Once); targetDbExecutor.Verify(t => t.ExecuteScriptAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } }