// Copyright (C) 2026, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Versioning; using Duplicati.Library.Logging; using Duplicati.Library.Snapshots; using Duplicati.Library.Snapshots.Windows; using NUnit.Framework; namespace Duplicati.UnitTest { [TestFixture] public class TestMSSQLParser { private const char PS = ';'; // Path separator between filters/sources private const char DS = '\\'; // Directory separator private const string MSSQL = "%MSSQL%"; /// /// Mock that returns a pre-populated set of databases without touching VSS. /// private sealed class MockMSSQLUtility : IMSSQLUtility { public Guid MSSQLWriterGuid { get; set; } = Guid.Parse("a65faa63-5ea8-4ebc-9dbd-a0c4db26912a"); public bool IsMSSQLInstalled { get; set; } = true; public List DBs { get; set; } = new(); public void QueryDBsInfo(WindowsSnapshotProvider provider) { // No-op; DBs are injected directly by the test } } private sealed class LogSink : ILogDestination { public List Entries { get; } = []; public void WriteMessage(LogEntry entry) => Entries.Add(entry); } private static MSSQLDB MakeDb(string server, string instance, string database, params string[] paths) => new MSSQLDB { Server = server, InstanceId = instance, Database = database, DataPaths = paths.ToList() }; private static Dictionary RequiredPolicyOptions() => new(StringComparer.OrdinalIgnoreCase) { ["snapshot-policy"] = "required" }; [SupportedOSPlatform("windows")] private static Dictionary Run(MockMSSQLUtility util, ref string[] paths, ref string filter, Dictionary options = null) { options ??= RequiredPolicyOptions(); using var _ = Log.StartScope(new LogSink()); return new Duplicati.Library.Modules.Builtin.MSSQLOptions() .RealParseSourcePaths(ref paths, ref filter, options, util); } [Test] public void Returns_empty_change_set_when_MSSQL_not_installed() { if (!OperatingSystem.IsWindows()) return; // MSSQL is only available on Windows var util = new MockMSSQLUtility { IsMSSQLInstalled = false }; var paths = new[] { MSSQL }; var filter = string.Empty; var result = Run(util, ref paths, ref filter); Assert.That(result, Is.Empty); Assert.That(paths, Is.EqualTo(new[] { MSSQL }), "Paths must stay untouched when MSSQL is not installed."); } [Test] public void All_marker_includes_every_database() { if (!OperatingSystem.IsWindows()) return; // MSSQL is only available on Windows var dbDefault = MakeDb("SRV1", "", "Sales", @$"C:{DS}Data{DS}Sales.mdf"); var dbInstance = MakeDb("SRV1", "INST", "HR", @$"C:{DS}Data{DS}HR.mdf"); var util = new MockMSSQLUtility { DBs = [dbDefault, dbInstance] }; var paths = new[] { MSSQL }; var filter = string.Empty; Run(util, ref paths, ref filter); Assert.That(paths, Does.Contain(dbDefault.DataPaths[0])); Assert.That(paths, Does.Contain(dbInstance.DataPaths[0])); Assert.That(paths, Does.Not.Contain(MSSQL), "Virtual marker must be replaced by real paths."); } [Test] public void Server_path_includes_all_databases_on_server() { if (!OperatingSystem.IsWindows()) return; // MSSQL is only available on Windows var dbDefault = MakeDb("SRV1", "", "Sales", @$"C:{DS}Data{DS}Sales.mdf"); var dbInstance = MakeDb("SRV1", "INST", "HR", @$"C:{DS}Data{DS}HR.mdf"); var dbOther = MakeDb("SRV2", "", "Other", @$"C:{DS}Data{DS}Other.mdf"); var util = new MockMSSQLUtility { DBs = [dbDefault, dbInstance, dbOther] }; var paths = new[] { $"{MSSQL}{DS}SRV1" }; var filter = string.Empty; Run(util, ref paths, ref filter); Assert.That(paths, Does.Contain(dbDefault.DataPaths[0]), "default-instance db on server missing"); Assert.That(paths, Does.Contain(dbInstance.DataPaths[0]), "named-instance db on server missing"); Assert.That(paths, Does.Not.Contain(dbOther.DataPaths[0]), "db from another server leaked in"); } [Test] public void Default_instance_database_path_resolves_single_database() { if (!OperatingSystem.IsWindows()) return; // MSSQL is only available on Windows // %MSSQL%\SRV1\Sales : 3 segments, default instance => Sales database var dbSales = MakeDb("SRV1", "", "Sales", @$"C:{DS}Data{DS}Sales.mdf"); var dbHr = MakeDb("SRV1", "", "HR", @$"C:{DS}Data{DS}HR.mdf"); var util = new MockMSSQLUtility { DBs = [dbSales, dbHr] }; var paths = new[] { $"{MSSQL}{DS}SRV1{DS}Sales" }; var filter = string.Empty; Run(util, ref paths, ref filter); Assert.That(paths, Does.Contain(dbSales.DataPaths[0]), "selected default-instance db missing"); Assert.That(paths, Does.Not.Contain(dbHr.DataPaths[0]), "non-selected db leaked in"); } [Test] public void Server_instance_path_includes_all_databases_in_instance() { if (!OperatingSystem.IsWindows()) return; // MSSQL is only available on Windows // %MSSQL%\SRV1\INST : 3 segments, matches a named instance => all its databases var dbInst1 = MakeDb("SRV1", "INST", "HR", @$"C:{DS}Data{DS}HR.mdf"); var dbInst2 = MakeDb("SRV1", "INST", "Payroll", @$"C:{DS}Data{DS}Payroll.mdf"); var dbDefault = MakeDb("SRV1", "", "Sales", @$"C:{DS}Data{DS}Sales.mdf"); var util = new MockMSSQLUtility { DBs = [dbInst1, dbInst2, dbDefault] }; var paths = new[] { $"{MSSQL}{DS}SRV1{DS}INST" }; var filter = string.Empty; Run(util, ref paths, ref filter); Assert.That(paths, Does.Contain(dbInst1.DataPaths[0])); Assert.That(paths, Does.Contain(dbInst2.DataPaths[0])); Assert.That(paths, Does.Not.Contain(dbDefault.DataPaths[0]), "default-instance db leaked in"); } [Test] public void Fully_qualified_path_resolves_single_database_on_named_instance() { if (!OperatingSystem.IsWindows()) return; // MSSQL is only available on Windows // %MSSQL%\SRV1\INST\HR : 4 segments var dbHr = MakeDb("SRV1", "INST", "HR", @$"C:{DS}Data{DS}HR.mdf"); var dbPayroll = MakeDb("SRV1", "INST", "Payroll", @$"C:{DS}Data{DS}Payroll.mdf"); var util = new MockMSSQLUtility { DBs = [dbHr, dbPayroll] }; var paths = new[] { $"{MSSQL}{DS}SRV1{DS}INST{DS}HR" }; var filter = string.Empty; Run(util, ref paths, ref filter); Assert.That(paths, Does.Contain(dbHr.DataPaths[0])); Assert.That(paths, Does.Not.Contain(dbPayroll.DataPaths[0])); } [Test] [SupportedOSPlatform("windows")] public void Ambiguous_instance_and_default_database_name_throws() { if (!OperatingSystem.IsWindows()) return; // MSSQL is only available on Windows // A named instance "INST" exists, plus a default-instance database also called "INST". // %MSSQL%\SRV1\INST is ambiguous (could mean the instance or the default-instance db), // so the parser must reject it rather than silently guessing. var dbInst = MakeDb("SRV1", "INST", "HR", @$"C:{DS}Data{DS}HR.mdf"); var dbDefaultNamedInst = MakeDb("SRV1", "", "INST", @$"C:{DS}Data{DS}DefaultInst.mdf"); var util = new MockMSSQLUtility { DBs = [dbInst, dbDefaultNamedInst] }; var paths = new[] { $"{MSSQL}{DS}SRV1{DS}INST" }; var filter = string.Empty; var ex = Assert.Throws(() => { var p = paths; var f = filter; Run(util, ref p, ref f); }); Assert.That(ex.HelpID, Is.EqualTo("MsSqlServerInstanceAmbiguous")); } [Test] public void Server_instance_match_wins_when_no_competing_default_database() { if (!OperatingSystem.IsWindows()) return; // MSSQL is only available on Windows // Only the named instance "INST" matches %MSSQL%\SRV1\INST; there is no // default-instance database called "INST", so the instance is selected unambiguously. var dbInst = MakeDb("SRV1", "INST", "HR", @$"C:{DS}Data{DS}HR.mdf"); var dbDefault = MakeDb("SRV1", "", "Sales", @$"C:{DS}Data{DS}Sales.mdf"); var util = new MockMSSQLUtility { DBs = [dbInst, dbDefault] }; var paths = new[] { $"{MSSQL}{DS}SRV1{DS}INST" }; var filter = string.Empty; Run(util, ref paths, ref filter); Assert.That(paths, Does.Contain(dbInst.DataPaths[0]), "instance match missing"); Assert.That(paths, Does.Not.Contain(dbDefault.DataPaths[0]), "unrelated default-instance db leaked in"); } [Test] [SupportedOSPlatform("windows")] public void Unknown_server_throws() { if (!OperatingSystem.IsWindows()) return; // MSSQL is only available on Windows var util = new MockMSSQLUtility { DBs = [MakeDb("SRV1", "", "Sales", @$"C:{DS}Data{DS}Sales.mdf")] }; var paths = new[] { $"{MSSQL}{DS}NOPE" }; var filter = string.Empty; var ex = Assert.Throws(() => { var p = paths; var f = filter; Run(util, ref p, ref f); }); Assert.That(ex.HelpID, Is.EqualTo("MsSqlServerNotFound")); } [Test] [SupportedOSPlatform("windows")] public void Unknown_instance_or_database_throws() { if (!OperatingSystem.IsWindows()) return; // MSSQL is only available on Windows // %MSSQL%\SRV1\NOPE matches neither a named instance nor a default-instance database var util = new MockMSSQLUtility { DBs = [MakeDb("SRV1", "", "Sales", @$"C:{DS}Data{DS}Sales.mdf")] }; var paths = new[] { $"{MSSQL}{DS}SRV1{DS}NOPE" }; var filter = string.Empty; var ex = Assert.Throws(() => { var p = paths; var f = filter; Run(util, ref p, ref f); }); Assert.That(ex.HelpID, Is.EqualTo("MsSqlServerInstanceNotFound")); } [Test] [SupportedOSPlatform("windows")] public void Unknown_database_on_named_instance_throws() { if (!OperatingSystem.IsWindows()) return; // MSSQL is only available on Windows var util = new MockMSSQLUtility { DBs = [MakeDb("SRV1", "INST", "HR", @$"C:{DS}Data{DS}HR.mdf")] }; var paths = new[] { $"{MSSQL}{DS}SRV1{DS}INST{DS}NOPE" }; var filter = string.Empty; var ex = Assert.Throws(() => { var p = paths; var f = filter; Run(util, ref p, ref f); }); Assert.That(ex.HelpID, Is.EqualTo("MsSqlDatabaseNotFound")); } [Test] public void Exclude_filter_removes_matching_database() { if (!OperatingSystem.IsWindows()) return; // MSSQL is only available on Windows var dbSales = MakeDb("SRV1", "", "Sales", @$"C:{DS}Data{DS}Sales.mdf"); var dbHr = MakeDb("SRV1", "", "HR", @$"C:{DS}Data{DS}HR.mdf"); var util = new MockMSSQLUtility { DBs = [dbSales, dbHr] }; var paths = new[] { MSSQL }; // Exclude HR on the default instance (no instance segment in the virtual path) var filter = $"-{MSSQL}{DS}SRV1{DS}HR"; Run(util, ref paths, ref filter); Assert.That(paths, Does.Contain(dbSales.DataPaths[0]), "unaffected db missing"); Assert.That(paths, Does.Not.Contain(dbHr.DataPaths[0]), "excluded db leaked in"); } [Test] public void Include_filter_keeps_only_matching_database() { if (!OperatingSystem.IsWindows()) return; // MSSQL is only available on Windows var dbSales = MakeDb("SRV1", "", "Sales", @$"C:{DS}Data{DS}Sales.mdf"); var dbHr = MakeDb("SRV1", "", "HR", @$"C:{DS}Data{DS}HR.mdf"); var util = new MockMSSQLUtility { DBs = [dbSales, dbHr] }; var paths = new[] { MSSQL }; // Include-only filter: anything not matched is excluded var filter = $"+{MSSQL}{DS}SRV1{DS}Sales"; Run(util, ref paths, ref filter); Assert.That(paths, Does.Contain(dbSales.DataPaths[0]), "included db missing"); Assert.That(paths, Does.Not.Contain(dbHr.DataPaths[0]), "non-included db leaked in"); } [Test] public void Exclude_filter_on_named_instance_database() { if (!OperatingSystem.IsWindows()) return; // MSSQL is only available on Windows var dbHr = MakeDb("SRV1", "INST", "HR", @$"C:{DS}Data{DS}HR.mdf"); var dbPayroll = MakeDb("SRV1", "INST", "Payroll", @$"C:{DS}Data{DS}Payroll.mdf"); var util = new MockMSSQLUtility { DBs = [dbHr, dbPayroll] }; var paths = new[] { $"{MSSQL}{DS}SRV1{DS}INST" }; var filter = $"-{MSSQL}{DS}SRV1{DS}INST{DS}Payroll"; Run(util, ref paths, ref filter); Assert.That(paths, Does.Contain(dbHr.DataPaths[0]), "unaffected named-instance db missing"); Assert.That(paths, Does.Not.Contain(dbPayroll.DataPaths[0]), "excluded named-instance db leaked in"); } [Test] public void Non_MSSQL_paths_and_filters_pass_through_untouched() { if (!OperatingSystem.IsWindows()) return; // MSSQL is only available on Windows var dbSales = MakeDb("SRV1", "", "Sales", @$"C:{DS}Data{DS}Sales.mdf"); var util = new MockMSSQLUtility { DBs = [dbSales] }; var ordinaryPath = @$"C:{DS}Users{DS}data"; var ordinaryFilter = $"-C:{DS}Users{DS}data{DS}temp"; var paths = new[] { MSSQL, ordinaryPath }; var filter = ordinaryFilter; Run(util, ref paths, ref filter); Assert.That(paths, Does.Contain(ordinaryPath), "ordinary source path must survive"); Assert.That(paths, Does.Contain(dbSales.DataPaths[0]), "db path must be added"); Assert.That(filter, Is.EqualTo(ordinaryFilter), "non-MSSQL filter must be left in the filter string"); } [Test] public void Handles_null_filter_gracefully() { if (!OperatingSystem.IsWindows()) return; // MSSQL is only available on Windows var dbSales = MakeDb("SRV1", "", "Sales", @$"C:{DS}Data{DS}Sales.mdf"); var util = new MockMSSQLUtility { DBs = [dbSales] }; var paths = new[] { MSSQL }; string filter = null; Run(util, ref paths, ref filter); Assert.That(paths, Does.Contain(dbSales.DataPaths[0])); } [Test] public void Enforces_required_snapshot_policy() { if (!OperatingSystem.IsWindows()) return; // MSSQL is only available on Windows var dbSales = MakeDb("SRV1", "", "Sales", @$"C:{DS}Data{DS}Sales.mdf"); var util = new MockMSSQLUtility { DBs = [dbSales] }; var paths = new[] { MSSQL }; var filter = string.Empty; var options = new Dictionary(StringComparer.OrdinalIgnoreCase); var result = Run(util, ref paths, ref filter, options); Assert.That(result.ContainsKey("snapshot-policy"), Is.True); Assert.That(result["snapshot-policy"], Is.EqualTo("required")); } } }