diff --git a/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs b/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs
new file mode 100644
index 00000000..b3647c39
--- /dev/null
+++ b/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs
@@ -0,0 +1,442 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using GeneralUpdate.Core;
+using GeneralUpdate.Core.Configuration;
+using GeneralUpdate.Core.FileSystem;
+using Xunit;
+
+namespace CoreTest.Bootstrap
+{
+ ///
+ /// Full parameter matrix tests -- verifies ALL UpdateOptions constants
+ /// can be set via .Option() without throwing. Covers 37 options across
+ /// core, deployment, silent, download, security, reporting, OSS, and
+ /// blacklist categories.
+ ///
+ public class BootstrapFullParameterMatrixTests : IDisposable
+ {
+ private readonly string _testDir;
+
+ public BootstrapFullParameterMatrixTests()
+ {
+ _testDir = Path.Combine(Path.GetTempPath(), $"GU_FullMatrix_{Guid.NewGuid()}");
+ Directory.CreateDirectory(_testDir);
+ }
+
+ public void Dispose()
+ {
+ try { Directory.Delete(_testDir, true); } catch { /* ignore */ }
+ }
+
+ private GeneralUpdateBootstrap B() => new GeneralUpdateBootstrap()
+ .SetConfig(new Configinfo
+ {
+ UpdateUrl = "https://api.example.com",
+ MainAppName = "MyApp.exe",
+ ClientVersion = "1.0.0",
+ InstallPath = _testDir,
+ AppSecretKey = "key",
+ Scheme = "https",
+ Token = "token"
+ });
+
+ #region Core
+ [Fact] public void AppType_Client() => Assert.NotNull(B().Option(UpdateOptions.AppType, AppType.Client));
+ [Fact] public void AppType_Upgrade() => Assert.NotNull(B().Option(UpdateOptions.AppType, AppType.Upgrade));
+ [Fact] public void AppType_OSS() => Assert.NotNull(B().Option(UpdateOptions.AppType, AppType.OSS));
+ [Fact] public void DiffMode_Serial() => Assert.NotNull(B().Option(UpdateOptions.DiffMode, DiffMode.Serial));
+ [Fact] public void DiffMode_Parallel() => Assert.NotNull(B().Option(UpdateOptions.DiffMode, DiffMode.Parallel));
+ [Fact] public void Encoding_Utf8() => Assert.NotNull(B().Option(UpdateOptions.Encoding, Encoding.UTF8));
+ [Fact] public void Encoding_Ascii() => Assert.NotNull(B().Option(UpdateOptions.Encoding, Encoding.ASCII));
+ [Fact] public void Format_ZIP() => Assert.NotNull(B().Option(UpdateOptions.Format, "ZIP"));
+ [Theory][InlineData(10)][InlineData(30)][InlineData(60)][InlineData(300)]
+ public void DownloadTimeout_Various(int t) => Assert.NotNull(B().Option(UpdateOptions.DownloadTimeout, t));
+ [Fact] public void DriveEnabled_True() => Assert.NotNull(B().Option(UpdateOptions.DriveEnabled, true));
+ [Fact] public void DriveEnabled_False() => Assert.NotNull(B().Option(UpdateOptions.DriveEnabled, false));
+ [Fact] public void PatchEnabled_True() => Assert.NotNull(B().Option(UpdateOptions.PatchEnabled, true));
+ [Fact] public void PatchEnabled_False() => Assert.NotNull(B().Option(UpdateOptions.PatchEnabled, false));
+ [Fact] public void BackupEnabled_False() => Assert.NotNull(B().Option(UpdateOptions.BackupEnabled, false));
+ [Fact] public void Silent_True() => Assert.NotNull(B().Option(UpdateOptions.Silent, true));
+ [Fact] public void Mode_Default() => Assert.NotNull(B().Option(UpdateOptions.Mode, UpdateMode.Default));
+ [Fact] public void Mode_Scripts() => Assert.NotNull(B().Option(UpdateOptions.Mode, UpdateMode.Scripts));
+ #endregion
+
+ #region Deployment
+ [Fact] public void UpdateUrl_Custom() => Assert.NotNull(B().Option(UpdateOptions.UpdateUrl, "https://update.company.com/api"));
+ [Fact] public void AppSecretKey_Custom() => Assert.NotNull(B().Option(UpdateOptions.AppSecretKey, "prod-secret"));
+ [Fact] public void AppName_Custom() => Assert.NotNull(B().Option(UpdateOptions.AppName, "Update.exe"));
+ [Fact] public void MainAppName_Custom() => Assert.NotNull(B().Option(UpdateOptions.MainAppName, "ProductApp.exe"));
+ [Fact] public void InstallPath_Custom() => Assert.NotNull(B().Option(UpdateOptions.InstallPath, _testDir));
+ [Fact] public void ClientVersion_Custom() => Assert.NotNull(B().Option(UpdateOptions.ClientVersion, "3.1.0-beta"));
+ [Fact] public void UpgradeClientVersion_Custom() => Assert.NotNull(B().Option(UpdateOptions.UpgradeClientVersion, "2.0.0"));
+ [Fact] public void Platform_Windows() => Assert.NotNull(B().Option(UpdateOptions.Platform, PlatformType.Windows));
+ [Fact] public void Platform_Linux() => Assert.NotNull(B().Option(UpdateOptions.Platform, PlatformType.Linux));
+ #endregion
+
+ #region Silent
+ [Fact] public void SilentAutoInstall_True() => Assert.NotNull(B().Option(UpdateOptions.SilentAutoInstall, true));
+ [Theory][InlineData(15)][InlineData(30)][InlineData(60)]
+ public void SilentPollInterval_Various(int m) => Assert.NotNull(B().Option(UpdateOptions.SilentPollIntervalMinutes, m));
+ #endregion
+
+ #region Download Performance
+ [Theory][InlineData(1)][InlineData(3)][InlineData(5)][InlineData(10)]
+ public void MaxConcurrency_Various(int c) => Assert.NotNull(B().Option(UpdateOptions.MaxConcurrency, c));
+ [Fact] public void EnableResume_False() => Assert.NotNull(B().Option(UpdateOptions.EnableResume, false));
+ [Theory][InlineData(1)][InlineData(3)][InlineData(5)]
+ public void RetryCount_Various(int c) => Assert.NotNull(B().Option(UpdateOptions.RetryCount, c));
+ [Fact] public void VerifyChecksum_False() => Assert.NotNull(B().Option(UpdateOptions.VerifyChecksum, false));
+ [Fact] public void RetryInterval_Custom() => Assert.NotNull(B().Option(UpdateOptions.RetryInterval, TimeSpan.FromSeconds(3)));
+ #endregion
+
+ #region Security
+ [Fact] public void Scheme_Bearer() => Assert.NotNull(B().Option(UpdateOptions.Scheme, "Bearer"));
+ [Fact] public void Scheme_ApiKey() => Assert.NotNull(B().Option(UpdateOptions.Scheme, "ApiKey"));
+ [Fact] public void Scheme_HMAC() => Assert.NotNull(B().Option(UpdateOptions.Scheme, "HMAC"));
+ [Fact] public void Token_Custom() => Assert.NotNull(B().Option(UpdateOptions.Token, "jwt-token-xyz"));
+ [Fact] public void PermissionScript_Custom() => Assert.NotNull(B().Option(UpdateOptions.PermissionScript, "#!/bin/bash\nchmod +x"));
+ #endregion
+
+ #region Reporting
+ [Fact] public void ReportUrl_Custom() => Assert.NotNull(B().Option(UpdateOptions.ReportUrl, "https://telemetry.example.com/report"));
+ [Fact] public void ProductId_Custom() => Assert.NotNull(B().Option(UpdateOptions.ProductId, "enterprise-pro"));
+ [Fact] public void UpdateLogUrl_Custom() => Assert.NotNull(B().Option(UpdateOptions.UpdateLogUrl, "https://myapp.com/releases"));
+ #endregion
+
+ #region OSS
+ [Fact] public void OSS_AliYun() => Assert.NotNull(B().Option(UpdateOptions.OSSProvider, OssProvider.AliYun));
+ [Fact] public void OSS_AWS() => Assert.NotNull(B().Option(UpdateOptions.OSSProvider, OssProvider.AWS));
+ [Fact] public void OSSBucketRegion() => Assert.NotNull(B().Option(UpdateOptions.OSSBucketRegion, "cn-shanghai"));
+ #endregion
+
+ #region Blacklist/Misc
+ [Fact] public void BlackList_Empty() => Assert.NotNull(B().Option(UpdateOptions.BlackList, BlackListConfig.Empty));
+ [Fact] public void BlackList_Configured() => Assert.NotNull(B().Option(UpdateOptions.BlackList,
+ new BlackListConfig(new List { "*.pdb" }, new List { ".log" }, new List { "logs" })));
+ [Fact] public void Bowl_Custom() => Assert.NotNull(B().Option(UpdateOptions.Bowl, "Bowl.exe"));
+ [Fact] public void Script_Custom() => Assert.NotNull(B().Option(UpdateOptions.Script, "chmod +x /app/Update"));
+ [Fact] public void Hub_Configured() => Assert.NotNull(B().Option(UpdateOptions.Hub,
+ new HubConfig { Url = "https://signalr.example.com/hub" }));
+ #endregion
+
+ #region Full Combination Chains
+ [Fact] public void Chain_All33Options()
+ {
+ var b = new GeneralUpdateBootstrap()
+ .Option(UpdateOptions.AppType, AppType.Client)
+ .Option(UpdateOptions.DiffMode, DiffMode.Parallel)
+ .Option(UpdateOptions.Encoding, Encoding.UTF8)
+ .Option(UpdateOptions.Format, "ZIP")
+ .Option(UpdateOptions.DownloadTimeout, 120)
+ .Option(UpdateOptions.DriveEnabled, false)
+ .Option(UpdateOptions.PatchEnabled, true)
+ .Option(UpdateOptions.BackupEnabled, true)
+ .Option(UpdateOptions.Mode, UpdateMode.Default)
+ .Option(UpdateOptions.Silent, false)
+ .Option(UpdateOptions.UpdateUrl, "https://update.example.com/api/v2")
+ .Option(UpdateOptions.AppSecretKey, "secret-key-2026")
+ .Option(UpdateOptions.AppName, "Update.exe")
+ .Option(UpdateOptions.MainAppName, "MyProduct.exe")
+ .Option(UpdateOptions.InstallPath, _testDir)
+ .Option(UpdateOptions.ClientVersion, "1.0.0")
+ .Option(UpdateOptions.UpgradeClientVersion, "0.5.0")
+ .Option(UpdateOptions.MaxConcurrency, 4)
+ .Option(UpdateOptions.EnableResume, true)
+ .Option(UpdateOptions.RetryCount, 5)
+ .Option(UpdateOptions.VerifyChecksum, true)
+ .Option(UpdateOptions.RetryInterval, TimeSpan.FromSeconds(2))
+ .Option(UpdateOptions.SilentAutoInstall, false)
+ .Option(UpdateOptions.SilentPollIntervalMinutes, 30)
+ .Option(UpdateOptions.ReportUrl, "https://telemetry.example.com/report")
+ .Option(UpdateOptions.ProductId, "my-product-001")
+ .Option(UpdateOptions.Scheme, "Bearer")
+ .Option(UpdateOptions.Token, "jwt-token-xyz")
+ .Option(UpdateOptions.PermissionScript, "#!/bin/bash\nchmod +x")
+ .Option(UpdateOptions.BlackList, BlackListConfig.Empty)
+ .Option(UpdateOptions.Bowl, "Bowl.exe")
+ .Option(UpdateOptions.UpdateLogUrl, "https://example.com/changelog")
+ .Option(UpdateOptions.Script, "chmod +x /opt/app/Update")
+ .SetConfig(new Configinfo
+ {
+ UpdateUrl = "https://update.example.com/api/v2",
+ MainAppName = "MyProduct.exe",
+ ClientVersion = "1.0.0",
+ InstallPath = _testDir,
+ AppSecretKey = "secret-key-2026",
+ Scheme = "Bearer",
+ Token = "jwt-token-xyz"
+ });
+ Assert.NotNull(b);
+ }
+
+ [Fact] public void Chain_SilentClient()
+ {
+ var b = new GeneralUpdateBootstrap()
+ .Option(UpdateOptions.AppType, AppType.Client)
+ .Option(UpdateOptions.Silent, true)
+ .Option(UpdateOptions.SilentAutoInstall, true)
+ .Option(UpdateOptions.SilentPollIntervalMinutes, 15)
+ .SetConfig(new Configinfo { UpdateUrl = "https://api.example.com", MainAppName = "MyApp.exe", ClientVersion = "1.0.0", InstallPath = _testDir, AppSecretKey = "key", Scheme = "https", Token = "token" });
+ Assert.NotNull(b);
+ }
+
+ [Fact] public void Chain_ParallelDiffHighConcurrency()
+ {
+ var b = new GeneralUpdateBootstrap()
+ .Option(UpdateOptions.DiffMode, DiffMode.Parallel).Option(UpdateOptions.MaxConcurrency, 8)
+ .SetConfig(new Configinfo { UpdateUrl = "https://api.example.com", MainAppName = "MyApp.exe", ClientVersion = "1.0.0", InstallPath = _testDir, AppSecretKey = "key", Scheme = "https", Token = "token" });
+ Assert.NotNull(b);
+ }
+
+ [Fact] public void Chain_UpgradeNoBackup()
+ {
+ var b = new GeneralUpdateBootstrap()
+ .Option(UpdateOptions.AppType, AppType.Upgrade).Option(UpdateOptions.BackupEnabled, false)
+ .Option(UpdateOptions.PatchEnabled, true).Option(UpdateOptions.VerifyChecksum, true)
+ .SetConfig(new Configinfo { UpdateUrl = "https://api.example.com", MainAppName = "MyApp.exe", ClientVersion = "1.0.0", InstallPath = _testDir, AppSecretKey = "key", Scheme = "https", Token = "token" });
+ Assert.NotNull(b);
+ }
+
+ [Fact] public void Chain_FullSecurity()
+ {
+ var b = new GeneralUpdateBootstrap()
+ .Option(UpdateOptions.Scheme, "HMAC").Option(UpdateOptions.Token, "hmac-secret")
+ .Option(UpdateOptions.ReportUrl, "https://telemetry.example.com/report")
+ .Option(UpdateOptions.VerifyChecksum, true)
+ .Option(UpdateOptions.PermissionScript, "#!/bin/bash\nchmod +x /opt/app/Update")
+ .SetConfig(new Configinfo { UpdateUrl = "https://secure.example.com/api", MainAppName = "SecureApp.exe", ClientVersion = "2.0.0", InstallPath = _testDir, AppSecretKey = "secure-key", Scheme = "HMAC", Token = "hmac-secret", ReportUrl = "https://telemetry.example.com/report" });
+ Assert.NotNull(b);
+ }
+
+ [Fact] public void Chain_UpgradeWithExtensions()
+ {
+ var b = new GeneralUpdateBootstrap()
+ .Option(UpdateOptions.AppType, AppType.Upgrade)
+ .Option(UpdateOptions.ReportUrl, "https://telemetry.example.com/report")
+ .Option(UpdateOptions.Scheme, "Bearer").Option(UpdateOptions.Token, "jwt")
+ .SetConfig(new Configinfo { UpdateUrl = "https://api.example.com", MainAppName = "MyApp.exe", ClientVersion = "1.0.0", InstallPath = _testDir, AppSecretKey = "key", Scheme = "Bearer", Token = "jwt" })
+ .AddListenerException((s, e) => { }).AddListenerUpdateInfo((s, e) => { })
+ .AddCustomOption(new List> { () => true });
+ Assert.NotNull(b);
+ }
+
+ ///
+ /// Complete production deployment: Client + Upgrade bootstraps configured
+ /// simultaneously with ALL non-conflicting parameters, hooks, listeners,
+ /// and extension points.
+ ///
+ [Fact]
+ public void Chain_ClientAndUpgrade_BothFullyConfigured()
+ {
+ var sharedConfig = new Configinfo
+ {
+ UpdateUrl = "https://update.enterprise.com/api/v2",
+ AppName = "Update.exe",
+ MainAppName = "EnterpriseApp.exe",
+ ClientVersion = "4.2.1",
+ UpgradeClientVersion = "2.0.0",
+ InstallPath = _testDir,
+ AppSecretKey = "enterprise-prod-key-2026",
+ ProductId = "enterprise-app-v4",
+ UpdateLogUrl = "https://enterprise.com/releases",
+ ReportUrl = "https://telemetry.enterprise.com/api/report",
+ Scheme = "HMAC",
+ Token = "hmac-prod-secret",
+ Bowl = "Bowl.exe",
+ Script = "#!/bin/bash\nset -e\nchmod +x /opt/enterprise/Update",
+ BlackFiles = new List { "*.pdb", "*.config" },
+ BlackFormats = new List { ".log", ".tmp", ".cache", ".etl" },
+ SkipDirectorys = new List { "logs", "temp", "cache", "diagnostics" }
+ };
+
+ var clientBootstrap = new GeneralUpdateBootstrap()
+ .Option(UpdateOptions.AppType, AppType.Client)
+ .Option(UpdateOptions.DiffMode, DiffMode.Parallel)
+ .Option(UpdateOptions.Encoding, Encoding.UTF8)
+ .Option(UpdateOptions.Format, "ZIP")
+ .Option(UpdateOptions.DownloadTimeout, 120)
+ .Option(UpdateOptions.DriveEnabled, false)
+ .Option(UpdateOptions.PatchEnabled, true)
+ .Option(UpdateOptions.BackupEnabled, true)
+ .Option(UpdateOptions.Mode, UpdateMode.Default)
+ .Option(UpdateOptions.Silent, false)
+ .Option(UpdateOptions.UpdateUrl, "https://update.enterprise.com/api/v2")
+ .Option(UpdateOptions.AppSecretKey, "enterprise-prod-key-2026")
+ .Option(UpdateOptions.AppName, "Update.exe")
+ .Option(UpdateOptions.MainAppName, "EnterpriseApp.exe")
+ .Option(UpdateOptions.InstallPath, _testDir)
+ .Option(UpdateOptions.ClientVersion, "4.2.1")
+ .Option(UpdateOptions.UpgradeClientVersion, "2.0.0")
+ .Option(UpdateOptions.Platform, PlatformType.Windows)
+ .Option(UpdateOptions.MaxConcurrency, 4)
+ .Option(UpdateOptions.EnableResume, true)
+ .Option(UpdateOptions.RetryCount, 5)
+ .Option(UpdateOptions.VerifyChecksum, true)
+ .Option(UpdateOptions.RetryInterval, TimeSpan.FromSeconds(2))
+ .Option(UpdateOptions.Scheme, "HMAC")
+ .Option(UpdateOptions.Token, "hmac-prod-secret")
+ .Option(UpdateOptions.PermissionScript, "#!/bin/bash\nchmod +x /opt/enterprise/Update")
+ .Option(UpdateOptions.ReportUrl, "https://telemetry.enterprise.com/api/report")
+ .Option(UpdateOptions.ProductId, "enterprise-app-v4")
+ .Option(UpdateOptions.UpdateLogUrl, "https://enterprise.com/releases")
+ .Option(UpdateOptions.BlackList, new BlackListConfig(
+ new List { "*.pdb", "*.config" },
+ new List { ".log", ".tmp" },
+ new List { "logs", "temp" }))
+ .Option(UpdateOptions.Bowl, "Bowl.exe")
+ .Option(UpdateOptions.Script, "chmod +x /opt/enterprise/Update")
+ .SetConfig(sharedConfig)
+ .AddListenerUpdatePrecheck(args =>
+ {
+ var hour = DateTime.Now.Hour;
+ return hour < 2 || hour > 6;
+ })
+ .AddListenerUpdateInfo((s, e) => { })
+ .AddListenerMultiAllDownloadCompleted((s, e) => { })
+ .AddListenerMultiDownloadCompleted((s, e) => { })
+ .AddListenerMultiDownloadError((s, e) => { })
+ .AddListenerMultiDownloadStatistics((s, e) => { })
+ .AddListenerException((s, e) => { })
+ .AddCustomOption(new List>
+ {
+ () => true, () => true, () => true
+ });
+
+ Assert.NotNull(clientBootstrap);
+
+ var upgradeBootstrap = new GeneralUpdateBootstrap()
+ .Option(UpdateOptions.AppType, AppType.Upgrade)
+ .Option(UpdateOptions.DiffMode, DiffMode.Parallel)
+ .Option(UpdateOptions.Encoding, Encoding.UTF8)
+ .Option(UpdateOptions.Format, "ZIP")
+ .Option(UpdateOptions.DownloadTimeout, 30)
+ .Option(UpdateOptions.DriveEnabled, true)
+ .Option(UpdateOptions.PatchEnabled, true)
+ .Option(UpdateOptions.BackupEnabled, false)
+ .Option(UpdateOptions.Mode, UpdateMode.Default)
+ .Option(UpdateOptions.AppName, "Update.exe")
+ .Option(UpdateOptions.MainAppName, "EnterpriseApp.exe")
+ .Option(UpdateOptions.InstallPath, _testDir)
+ .Option(UpdateOptions.ClientVersion, "4.2.1")
+ .Option(UpdateOptions.Platform, PlatformType.Windows)
+ .Option(UpdateOptions.MaxConcurrency, 2)
+ .Option(UpdateOptions.VerifyChecksum, true)
+ .Option(UpdateOptions.RetryInterval, TimeSpan.FromSeconds(1))
+ .Option(UpdateOptions.Scheme, "HMAC")
+ .Option(UpdateOptions.Token, "hmac-prod-secret")
+ .Option(UpdateOptions.PermissionScript, "#!/bin/bash\nchmod +x /opt/enterprise/Update")
+ .Option(UpdateOptions.ReportUrl, "https://telemetry.enterprise.com/api/report")
+ .Option(UpdateOptions.ProductId, "enterprise-app-v4")
+ .Option(UpdateOptions.BlackList, BlackListConfig.Empty)
+ .Option(UpdateOptions.Script, "chmod +x /opt/enterprise/Update")
+ .SetConfig(sharedConfig)
+ .AddListenerException((s, e) => { })
+ .AddListenerUpdateInfo((s, e) => { });
+
+ Assert.NotNull(upgradeBootstrap);
+ Assert.NotSame(clientBootstrap, upgradeBootstrap);
+ }
+
+ ///
+ /// Real-world developer workflow: configure both Client and Upgrade
+ /// bootstraps with hooks, reporter, and full extension chain.
+ ///
+ [Fact]
+ public void Chain_ClientAndUpgrade_CompleteDeveloperWorkflow()
+ {
+ var installPath = _testDir;
+ var updateUrl = "https://update.myapp.com/api";
+ var mainApp = "MyApp.exe";
+ var currentVersion = "3.0.0";
+
+ var client = new GeneralUpdateBootstrap()
+ .Option(UpdateOptions.AppType, AppType.Client)
+ .Option(UpdateOptions.DiffMode, DiffMode.Parallel)
+ .Option(UpdateOptions.UpdateUrl, updateUrl)
+ .Option(UpdateOptions.AppName, "Update.exe")
+ .Option(UpdateOptions.MainAppName, mainApp)
+ .Option(UpdateOptions.InstallPath, installPath)
+ .Option(UpdateOptions.ClientVersion, currentVersion)
+ .Option(UpdateOptions.UpgradeClientVersion, "2.0.0")
+ .Option(UpdateOptions.Encoding, Encoding.UTF8)
+ .Option(UpdateOptions.Format, "ZIP")
+ .Option(UpdateOptions.DownloadTimeout, 120)
+ .Option(UpdateOptions.PatchEnabled, true)
+ .Option(UpdateOptions.BackupEnabled, true)
+ .Option(UpdateOptions.MaxConcurrency, 4)
+ .Option(UpdateOptions.EnableResume, true)
+ .Option(UpdateOptions.RetryCount, 5)
+ .Option(UpdateOptions.VerifyChecksum, true)
+ .Option(UpdateOptions.RetryInterval, TimeSpan.FromSeconds(2))
+ .Option(UpdateOptions.Scheme, "Bearer")
+ .Option(UpdateOptions.Token, "client-jwt")
+ .Option(UpdateOptions.ReportUrl, "https://telemetry.myapp.com/report")
+ .Option(UpdateOptions.ProductId, "myapp-pro")
+ .Option(UpdateOptions.UpdateLogUrl, "https://myapp.com/changelog")
+ .Option(UpdateOptions.BlackList, new BlackListConfig(
+ new List { "*.pdb" },
+ new List { ".log", ".tmp" },
+ new List { "logs", "temp" }))
+ .Option(UpdateOptions.Bowl, "Bowl.exe")
+ .Option(UpdateOptions.Platform, PlatformType.Windows)
+ .SetConfig(new Configinfo
+ {
+ UpdateUrl = updateUrl, AppName = "Update.exe", MainAppName = mainApp,
+ ClientVersion = currentVersion, UpgradeClientVersion = "2.0.0",
+ InstallPath = installPath, AppSecretKey = "myapp-key", ProductId = "myapp-pro",
+ Scheme = "Bearer", Token = "client-jwt",
+ ReportUrl = "https://telemetry.myapp.com/report",
+ UpdateLogUrl = "https://myapp.com/changelog", Bowl = "Bowl.exe",
+ BlackFiles = new List { "*.pdb" },
+ BlackFormats = new List { ".log", ".tmp" },
+ SkipDirectorys = new List { "logs", "temp" }
+ })
+ .AddListenerUpdateInfo((s, e) => { })
+ .AddListenerMultiDownloadCompleted((s, e) => { })
+ .AddListenerMultiAllDownloadCompleted((s, e) => { })
+ .AddListenerMultiDownloadError((s, e) => { })
+ .AddListenerMultiDownloadStatistics((s, e) => { })
+ .AddListenerException((s, e) => { })
+ .AddListenerUpdatePrecheck(args => false)
+ .AddCustomOption(new List> { () => Directory.Exists(installPath), () => true });
+
+ Assert.NotNull(client);
+
+ var upgrade = new GeneralUpdateBootstrap()
+ .Option(UpdateOptions.AppType, AppType.Upgrade)
+ .Option(UpdateOptions.DiffMode, DiffMode.Parallel)
+ .Option(UpdateOptions.AppName, "Update.exe")
+ .Option(UpdateOptions.MainAppName, mainApp)
+ .Option(UpdateOptions.InstallPath, installPath)
+ .Option(UpdateOptions.ClientVersion, currentVersion)
+ .Option(UpdateOptions.Encoding, Encoding.UTF8)
+ .Option(UpdateOptions.Format, "ZIP")
+ .Option(UpdateOptions.PatchEnabled, true)
+ .Option(UpdateOptions.VerifyChecksum, true)
+ .Option(UpdateOptions.MaxConcurrency, 2)
+ .Option(UpdateOptions.RetryCount, 3)
+ .Option(UpdateOptions.RetryInterval, TimeSpan.FromSeconds(1))
+ .Option(UpdateOptions.Scheme, "Bearer")
+ .Option(UpdateOptions.Token, "upgrade-jwt")
+ .Option(UpdateOptions.ReportUrl, "https://telemetry.myapp.com/report")
+ .Option(UpdateOptions.ProductId, "myapp-pro")
+ .Option(UpdateOptions.BlackList, BlackListConfig.Empty)
+ .Option(UpdateOptions.Platform, PlatformType.Windows)
+ .SetConfig(new Configinfo
+ {
+ UpdateUrl = updateUrl, AppName = "Update.exe", MainAppName = mainApp,
+ ClientVersion = currentVersion, InstallPath = installPath,
+ AppSecretKey = "myapp-key", Scheme = "Bearer", Token = "upgrade-jwt",
+ ReportUrl = "https://telemetry.myapp.com/report"
+ })
+ .AddListenerException((s, e) => { })
+ .AddListenerUpdateInfo((s, e) => { });
+
+ Assert.NotNull(upgrade);
+ Assert.NotSame(client, upgrade);
+ }
+ #endregion
+ }
+}
diff --git a/tests/CoreTest/Bootstrap/BootstrapHooksAndExtensionsTests.cs b/tests/CoreTest/Bootstrap/BootstrapHooksAndExtensionsTests.cs
new file mode 100644
index 00000000..612416cd
--- /dev/null
+++ b/tests/CoreTest/Bootstrap/BootstrapHooksAndExtensionsTests.cs
@@ -0,0 +1,486 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using GeneralUpdate.Core;
+using GeneralUpdate.Core.Configuration;
+using GeneralUpdate.Core.Download;
+using GeneralUpdate.Core.Download.Models;
+using GeneralUpdate.Core.Download.Reporting;
+using GeneralUpdate.Core.Event;
+using GeneralUpdate.Core.Hooks;
+using Xunit;
+
+namespace CoreTest.Bootstrap
+{
+ ///
+ /// Integration tests for Hooks mechanism, UpdateReporter,
+ /// IUpdateEventListener, and extension point models.
+ ///
+ /// Covers:
+ /// - IUpdateHooks lifecycle (OnBeforeUpdate, OnDownloadCompleted, OnAfterUpdate, OnError, OnBeforeStartApp)
+ /// - Custom IUpdateHooks implementation with lifecycle tracking
+ /// - NoOpUpdateHooks default behavior
+ /// - UpdateContext and DownloadContext data models
+ /// - IUpdateReporter / UpdateReport / UpdateEvent types
+ /// - IUpdateEventListener batch listener
+ /// - Security/Scheme extensibility via UpdateOptions
+ /// - HubConfig model
+ ///
+ public class BootstrapHooksAndExtensionsTests : IDisposable
+ {
+ private readonly string _testDir;
+
+ public BootstrapHooksAndExtensionsTests()
+ {
+ _testDir = Path.Combine(Path.GetTempPath(), $"GU_HooksExt_{Guid.NewGuid()}");
+ Directory.CreateDirectory(_testDir);
+ }
+
+ public void Dispose()
+ {
+ try { Directory.Delete(_testDir, true); } catch { /* ignore */ }
+ }
+
+ #region Custom TrackingHooks
+
+ private sealed class TrackingHooks : IUpdateHooks
+ {
+ public bool BeforeUpdateCalled { get; private set; }
+ public bool DownloadCompletedCalled { get; private set; }
+ public bool AfterUpdateCalled { get; private set; }
+ public bool ErrorCalled { get; private set; }
+ public bool BeforeStartAppCalled { get; private set; }
+
+ public UpdateContext? BeforeCtx { get; private set; }
+ public DownloadContext? DownloadCtx { get; private set; }
+ public Exception? CapturedError { get; private set; }
+
+ public Task OnBeforeUpdateAsync(UpdateContext ctx)
+ {
+ BeforeUpdateCalled = true;
+ BeforeCtx = ctx;
+ return Task.FromResult(true);
+ }
+
+ public Task OnDownloadCompletedAsync(DownloadContext ctx)
+ {
+ DownloadCompletedCalled = true;
+ DownloadCtx = ctx;
+ return Task.CompletedTask;
+ }
+
+ public Task OnAfterUpdateAsync(UpdateContext ctx)
+ {
+ AfterUpdateCalled = true;
+ return Task.CompletedTask;
+ }
+
+ public Task OnUpdateErrorAsync(UpdateContext ctx, Exception ex)
+ {
+ ErrorCalled = true;
+ CapturedError = ex;
+ return Task.CompletedTask;
+ }
+
+ public Task OnBeforeStartAppAsync(UpdateContext ctx)
+ {
+ BeforeStartAppCalled = true;
+ return Task.CompletedTask;
+ }
+ }
+
+ #endregion
+
+ #region Hook Lifecycle
+
+ [Fact]
+ public void TrackingHooks_InitialState_AllFlagsFalse()
+ {
+ var hooks = new TrackingHooks();
+ Assert.False(hooks.BeforeUpdateCalled);
+ Assert.False(hooks.DownloadCompletedCalled);
+ Assert.False(hooks.AfterUpdateCalled);
+ Assert.False(hooks.ErrorCalled);
+ Assert.False(hooks.BeforeStartAppCalled);
+ }
+
+ [Fact]
+ public async Task TrackingHooks_OnBeforeUpdate_ReturnsTrueAndRecordsContext()
+ {
+ var hooks = new TrackingHooks();
+ var ctx = new UpdateContext("MyApp.exe", "/install", "1.0.0", "2.0.0", AppType.Client);
+
+ var result = await hooks.OnBeforeUpdateAsync(ctx);
+
+ Assert.True(result);
+ Assert.True(hooks.BeforeUpdateCalled);
+ Assert.NotNull(hooks.BeforeCtx);
+ Assert.Equal("MyApp.exe", hooks.BeforeCtx.AppName);
+ Assert.Equal("1.0.0", hooks.BeforeCtx.CurrentVersion);
+ Assert.Equal("2.0.0", hooks.BeforeCtx.TargetVersion);
+ Assert.Equal(AppType.Client, hooks.BeforeCtx.AppType);
+ }
+
+ [Fact]
+ public async Task TrackingHooks_OnBeforeUpdate_CanReject()
+ {
+ var rejectingHooks = new RejectingHooks();
+ var ctx = new UpdateContext("App.exe", "/app", "1.0.0", "2.0.0", AppType.Client);
+
+ var result = await rejectingHooks.OnBeforeUpdateAsync(ctx);
+
+ Assert.False(result, "Hook should be able to reject update");
+ }
+
+ private sealed class RejectingHooks : IUpdateHooks
+ {
+ public Task OnBeforeUpdateAsync(UpdateContext ctx) => Task.FromResult(false);
+ public Task OnDownloadCompletedAsync(DownloadContext ctx) => Task.CompletedTask;
+ public Task OnAfterUpdateAsync(UpdateContext ctx) => Task.CompletedTask;
+ public Task OnUpdateErrorAsync(UpdateContext ctx, Exception ex) => Task.CompletedTask;
+ public Task OnBeforeStartAppAsync(UpdateContext ctx) => Task.CompletedTask;
+ }
+
+ [Fact]
+ public async Task TrackingHooks_OnDownloadCompleted_Success()
+ {
+ var hooks = new TrackingHooks();
+ var ctx = new DownloadContext("update.zip", "2.0.0", 50 * 1024 * 1024L, TimeSpan.FromSeconds(30), "/tmp/update.zip", true);
+
+ await hooks.OnDownloadCompletedAsync(ctx);
+
+ Assert.True(hooks.DownloadCompletedCalled);
+ Assert.NotNull(hooks.DownloadCtx);
+ Assert.Equal("update.zip", hooks.DownloadCtx.AssetName);
+ Assert.Equal("2.0.0", hooks.DownloadCtx.Version);
+ Assert.True(hooks.DownloadCtx.Success);
+ }
+
+ [Fact]
+ public async Task TrackingHooks_OnDownloadCompleted_Failure()
+ {
+ var hooks = new TrackingHooks();
+ var ctx = new DownloadContext("corrupt.zip", "2.0.0", 0, TimeSpan.FromSeconds(5), null, false);
+
+ await hooks.OnDownloadCompletedAsync(ctx);
+
+ Assert.True(hooks.DownloadCompletedCalled);
+ Assert.NotNull(hooks.DownloadCtx);
+ Assert.False(hooks.DownloadCtx.Success);
+ }
+
+ [Fact]
+ public async Task TrackingHooks_OnAfterUpdate_RecordsCall()
+ {
+ var hooks = new TrackingHooks();
+ var ctx = new UpdateContext("MyApp.exe", "/install", "1.0.0", "2.0.0", AppType.Client);
+
+ await hooks.OnAfterUpdateAsync(ctx);
+
+ Assert.True(hooks.AfterUpdateCalled);
+ }
+
+ [Fact]
+ public async Task TrackingHooks_OnUpdateError_CapturesException()
+ {
+ var hooks = new TrackingHooks();
+ var ctx = new UpdateContext("MyApp.exe", "/install", "1.0.0", "2.0.0", AppType.Client);
+ var ex = new InvalidOperationException("Hash verification failed");
+
+ await hooks.OnUpdateErrorAsync(ctx, ex);
+
+ Assert.True(hooks.ErrorCalled);
+ Assert.NotNull(hooks.CapturedError);
+ Assert.Equal("Hash verification failed", hooks.CapturedError.Message);
+ }
+
+ [Fact]
+ public async Task TrackingHooks_OnBeforeStartApp_RecordsCall()
+ {
+ var hooks = new TrackingHooks();
+ var ctx = new UpdateContext("MyApp.exe", "/install", "2.0.0", null, AppType.Client);
+
+ await hooks.OnBeforeStartAppAsync(ctx);
+
+ Assert.True(hooks.BeforeStartAppCalled);
+ }
+
+ [Fact]
+ public async Task TrackingHooks_FullLifecycle_AllFiveMethodsCalled()
+ {
+ var hooks = new TrackingHooks();
+ var beforeCtx = new UpdateContext("App.exe", "/app", "1.0.0", "2.0.0", AppType.Client);
+ var downloadCtx = new DownloadContext("pkg.zip", "2.0.0", 100, TimeSpan.FromSeconds(10), "/tmp/pkg.zip", true);
+ var afterCtx = new UpdateContext("App.exe", "/app", "2.0.0", null, AppType.Client);
+
+ await hooks.OnBeforeUpdateAsync(beforeCtx);
+ await hooks.OnDownloadCompletedAsync(downloadCtx);
+ await hooks.OnAfterUpdateAsync(afterCtx);
+ await hooks.OnBeforeStartAppAsync(afterCtx);
+
+ Assert.True(hooks.BeforeUpdateCalled);
+ Assert.True(hooks.DownloadCompletedCalled);
+ Assert.True(hooks.AfterUpdateCalled);
+ Assert.True(hooks.BeforeStartAppCalled);
+ Assert.False(hooks.ErrorCalled); // No error injected
+ }
+
+ #endregion
+
+ #region NoOpUpdateHooks
+
+ [Fact]
+ public async Task NoOpUpdateHooks_AllMethods_ReturnDefaults()
+ {
+ var hooks = new NoOpUpdateHooks();
+ var ctx = new UpdateContext("App.exe", "/app", "1.0.0", "2.0.0", AppType.Client);
+ var dlCtx = new DownloadContext("pkg.zip", "2.0.0", 100, TimeSpan.FromSeconds(10), "/tmp/pkg.zip", true);
+
+ var beforeResult = await hooks.OnBeforeUpdateAsync(ctx);
+ Assert.True(beforeResult);
+
+ await hooks.OnDownloadCompletedAsync(dlCtx);
+ await hooks.OnAfterUpdateAsync(ctx);
+ await hooks.OnUpdateErrorAsync(ctx, new Exception("test"));
+ await hooks.OnBeforeStartAppAsync(ctx);
+ // No-op hooks should never throw
+ }
+
+ #endregion
+
+ #region UpdateContext & DownloadContext
+
+ [Fact]
+ public void UpdateContext_AllFields_SetCorrectly()
+ {
+ var ctx = new UpdateContext("MyApp.exe", "/opt/app", "3.2.1", "4.0.0", AppType.Client);
+
+ Assert.Equal("MyApp.exe", ctx.AppName);
+ Assert.Equal("/opt/app", ctx.InstallPath);
+ Assert.Equal("3.2.1", ctx.CurrentVersion);
+ Assert.Equal("4.0.0", ctx.TargetVersion);
+ Assert.Equal(AppType.Client, ctx.AppType);
+ }
+
+ [Fact]
+ public void UpdateContext_UpgradeType()
+ {
+ var ctx = new UpdateContext("Update.exe", "/opt/updater", "1.0.0", "1.5.0", AppType.Upgrade);
+ Assert.Equal(AppType.Upgrade, ctx.AppType);
+ }
+
+ [Fact]
+ public void UpdateContext_TargetVersionCanBeNull()
+ {
+ var ctx = new UpdateContext("App.exe", "/app", "2.0.0", null, AppType.Client);
+ Assert.Null(ctx.TargetVersion);
+ }
+
+ [Fact]
+ public void DownloadContext_Successful()
+ {
+ var ctx = new DownloadContext("client-v2.0.0.zip", "2.0.0",
+ 1024L * 1024 * 50, TimeSpan.FromMinutes(2), "/tmp/update/client-v2.0.0.zip", true);
+
+ Assert.Equal("client-v2.0.0.zip", ctx.AssetName);
+ Assert.Equal("2.0.0", ctx.Version);
+ Assert.Equal(52428800, ctx.TotalBytes);
+ Assert.Equal(TimeSpan.FromMinutes(2), ctx.Duration);
+ Assert.Equal("/tmp/update/client-v2.0.0.zip", ctx.LocalPath);
+ Assert.True(ctx.Success);
+ }
+
+ [Fact]
+ public void DownloadContext_Failed()
+ {
+ var ctx = new DownloadContext("failed-pkg.zip", "2.0.0", 0, TimeSpan.FromSeconds(3), null, false);
+
+ Assert.False(ctx.Success);
+ Assert.Null(ctx.LocalPath);
+ }
+
+ #endregion
+
+ #region UpdateReport / IUpdateReporter
+
+ [Fact]
+ public void UpdateReport_StartedEvent()
+ {
+ var report = new UpdateReport("MyApp.exe", "1.0.0", "2.0.0",
+ UpdateEvent.UpdateStarted, AppType.Client,
+ DateTimeOffset.UtcNow);
+
+ Assert.Equal(UpdateEvent.UpdateStarted, report.Event);
+ Assert.Equal("MyApp.exe", report.AppName);
+ Assert.Equal("1.0.0", report.FromVersion);
+ Assert.Equal("2.0.0", report.ToVersion);
+ Assert.Equal(AppType.Client, report.AppType);
+ }
+
+ [Fact]
+ public void UpdateReport_FailedWithError()
+ {
+ var report = new UpdateReport("MyApp.exe", "1.0.0", "2.0.0",
+ UpdateEvent.UpdateFailed, AppType.Client,
+ DateTimeOffset.UtcNow,
+ ErrorMessage: "Disk space insufficient",
+ DurationMs: 15000.0);
+
+ Assert.Equal(UpdateEvent.UpdateFailed, report.Event);
+ Assert.Equal("Disk space insufficient", report.ErrorMessage);
+ Assert.Equal(15000.0, report.DurationMs);
+ }
+
+ [Fact]
+ public void UpdateReport_DownloadCompletedWithDuration()
+ {
+ var report = new UpdateReport("MyApp.exe", "1.0.0", "2.0.0",
+ UpdateEvent.DownloadCompleted, AppType.Client,
+ DateTimeOffset.UtcNow,
+ DurationMs: 45200.5);
+
+ Assert.Equal(UpdateEvent.DownloadCompleted, report.Event);
+ Assert.Equal(45200.5, report.DurationMs);
+ }
+
+ [Fact]
+ public void UpdateEvent_AllValues_AreDefined()
+ {
+ var values = Enum.GetValues();
+ Assert.Contains(UpdateEvent.UpdateStarted, values);
+ Assert.Contains(UpdateEvent.DownloadCompleted, values);
+ Assert.Contains(UpdateEvent.UpdateApplied, values);
+ Assert.Contains(UpdateEvent.UpdateFailed, values);
+ Assert.Contains(UpdateEvent.AppStarted, values);
+ }
+
+ #endregion
+
+ #region IUpdateEventListener
+
+ [Fact]
+ public void UpdateEventListener_AllMethods_AreCallable()
+ {
+ var listener = new TestEventListener();
+ var versionInfo = new VersionInfo { Version = "2.0.0", Url = "https://cdn.example.com/pkg.zip", Format = "ZIP" };
+
+ listener.OnAllDownloadCompleted(new MultiAllDownloadCompletedEventArgs(true, new List<(object, string)>()));
+ listener.OnDownloadCompleted(new MultiDownloadCompletedEventArgs(versionInfo, true));
+ listener.OnDownloadError(new MultiDownloadErrorEventArgs(new Exception("test"), versionInfo));
+ listener.OnDownloadStatistics(new MultiDownloadStatisticsEventArgs(versionInfo, TimeSpan.Zero, "0 B/s", 0, 0, 0));
+ listener.OnUpdateInfo(new UpdateInfoEventArgs(new VersionRespDTO { Code = 200 }));
+ listener.OnException(new ExceptionEventArgs(new Exception("test"), "test"));
+ listener.OnProgress(new ProgressEventArgs(new DownloadProgress("update.zip", 50L * 1024 * 1024, 100L * 1024 * 1024, 50.0, DownloadStatus.Downloading)));
+ Assert.True(listener.AllDownloadCalled);
+ Assert.True(listener.DownloadCompletedCalled);
+ Assert.True(listener.DownloadErrorCalled);
+ Assert.True(listener.StatisticsCalled);
+ Assert.True(listener.UpdateInfoCalled);
+ Assert.True(listener.ExceptionCalled);
+ Assert.True(listener.ProgressCalled);
+ }
+
+ private sealed class TestEventListener : IUpdateEventListener
+ {
+ public bool AllDownloadCalled { get; private set; }
+ public bool DownloadCompletedCalled { get; private set; }
+ public bool DownloadErrorCalled { get; private set; }
+ public bool StatisticsCalled { get; private set; }
+ public bool UpdateInfoCalled { get; private set; }
+ public bool ExceptionCalled { get; private set; }
+ public bool ProgressCalled { get; private set; }
+ public bool CustomEventCalled { get; private set; }
+
+ public void OnAllDownloadCompleted(MultiAllDownloadCompletedEventArgs e) => AllDownloadCalled = true;
+ public void OnDownloadCompleted(MultiDownloadCompletedEventArgs e) => DownloadCompletedCalled = true;
+ public void OnDownloadError(MultiDownloadErrorEventArgs e) => DownloadErrorCalled = true;
+ public void OnDownloadStatistics(MultiDownloadStatisticsEventArgs e) => StatisticsCalled = true;
+ public void OnUpdateInfo(UpdateInfoEventArgs e) => UpdateInfoCalled = true;
+ public void OnException(ExceptionEventArgs e) => ExceptionCalled = true;
+ public void OnProgress(ProgressEventArgs e) => ProgressCalled = true;
+ public void OnCustomEvent(string eventName, EventArgs e) => CustomEventCalled = true;
+ }
+
+ #endregion
+
+ #region Security Extensibility
+
+ [Fact]
+ public void AuthSchemeAndToken_CanBeConfigured()
+ {
+ var b = new GeneralUpdateBootstrap()
+ .Option(UpdateOptions.Scheme, "Bearer")
+ .Option(UpdateOptions.Token, "jwt-token-abc123")
+ .SetConfig(new Configinfo
+ {
+ UpdateUrl = "https://api.example.com",
+ MainAppName = "MyApp.exe",
+ ClientVersion = "1.0.0",
+ InstallPath = _testDir,
+ AppSecretKey = "key",
+ Scheme = "Bearer",
+ Token = "jwt-token-abc123"
+ });
+ Assert.NotNull(b);
+ }
+
+ [Fact]
+ public void AllAuthSchemes_CanBeConfigured()
+ {
+ foreach (var scheme in new[] { "Bearer", "ApiKey", "Basic", "HMAC" })
+ {
+ var b = new GeneralUpdateBootstrap()
+ .Option(UpdateOptions.Scheme, scheme)
+ .Option(UpdateOptions.Token, "test-token")
+ .SetConfig(new Configinfo
+ {
+ UpdateUrl = "https://api.example.com",
+ MainAppName = "MyApp.exe",
+ ClientVersion = "1.0.0",
+ InstallPath = _testDir,
+ AppSecretKey = "key",
+ Scheme = scheme,
+ Token = "test-token"
+ });
+ Assert.NotNull(b);
+ }
+ }
+
+ #endregion
+
+ #region Full Extension Chain
+
+ [Fact]
+ public void Bootstrap_FullExtensions_AllConfigured()
+ {
+ var b = new GeneralUpdateBootstrap()
+ .Option(UpdateOptions.AppType, AppType.Client)
+ .Option(UpdateOptions.UpdateUrl, "https://update.example.com/api")
+ .Option(UpdateOptions.ReportUrl, "https://telemetry.example.com/report")
+ .Option(UpdateOptions.Scheme, "Bearer")
+ .Option(UpdateOptions.Token, "jwt-token")
+ .Option(UpdateOptions.PermissionScript, "#!/bin/bash\nchmod +x /opt/app/Update")
+ .SetConfig(new Configinfo
+ {
+ UpdateUrl = "https://update.example.com/api",
+ MainAppName = "MyApp.exe",
+ ClientVersion = "1.0.0",
+ InstallPath = _testDir,
+ AppSecretKey = "key",
+ Scheme = "Bearer",
+ Token = "jwt-token",
+ ReportUrl = "https://telemetry.example.com/report"
+ })
+ .AddListenerUpdateInfo((s, e) => { })
+ .AddListenerException((s, e) => { })
+ .AddListenerMultiAllDownloadCompleted((s, e) => { })
+ .AddListenerUpdatePrecheck(args => false)
+ .AddCustomOption(new List> { () => true });
+
+ Assert.NotNull(b);
+ }
+
+ #endregion
+ }
+}
diff --git a/tests/CoreTest/Configuration/ConfigurationModelsTests.cs b/tests/CoreTest/Configuration/ConfigurationModelsTests.cs
new file mode 100644
index 00000000..89a936e3
--- /dev/null
+++ b/tests/CoreTest/Configuration/ConfigurationModelsTests.cs
@@ -0,0 +1,371 @@
+using System;
+using System.Collections.Generic;
+using GeneralUpdate.Core.Configuration;
+using GeneralUpdate.Core.Download;
+using GeneralUpdate.Core.Download.Reporting;
+using GeneralUpdate.Core.Download.Models;
+using GeneralUpdate.Core.FileSystem;
+using Xunit;
+
+namespace CoreTest.Configuration
+{
+ ///
+ /// Unit tests for configuration models used across the update framework.
+ /// Covers:
+ /// - BlackListConfig (new config-based blacklist with IReadOnlyList)
+ /// - HubConfig (SignalR hub configuration)
+ /// - DownloadAsset / DownloadPlan / DownloadProgress / DownloadResult (download pipeline models)
+ /// - DownloadStatus / DownloadPriority enums
+ /// - AppType / DiffMode / UpdateMode / PlatformType / OssProvider enums
+ /// - UpdateOption<T> value semantics
+ /// - UpdateReport / UpdateEvent types
+ ///
+ public class ConfigurationModelsTests
+ {
+ #region BlackListConfig
+
+ [Fact]
+ public void BlackListConfig_Empty_HasAllNullLists()
+ {
+ var config = BlackListConfig.Empty;
+ Assert.NotNull(config);
+ Assert.Null(config.BlackFiles);
+ Assert.Null(config.BlackFormats);
+ Assert.Null(config.SkipDirectorys);
+ }
+
+ [Fact]
+ public void BlackListConfig_WithAllFields_SetsCorrectly()
+ {
+ var config = new BlackListConfig(
+ BlackFiles: new List { "*.pdb", "*.config", "secret.key" },
+ BlackFormats: new List { ".log", ".tmp", ".cache" },
+ SkipDirectorys: new List { "logs", "temp", "__backups__" }
+ );
+
+ Assert.NotNull(config.BlackFiles);
+ Assert.Equal(3, config.BlackFiles.Count);
+ Assert.Contains("*.pdb", config.BlackFiles);
+ Assert.NotNull(config.BlackFormats);
+ Assert.Equal(3, config.BlackFormats.Count);
+ Assert.NotNull(config.SkipDirectorys);
+ Assert.Equal(3, config.SkipDirectorys.Count);
+ }
+
+ [Fact]
+ public void BlackListConfig_Partial_SingleListOnly()
+ {
+ var config = new BlackListConfig(
+ BlackFiles: new List { "*.pdb" },
+ BlackFormats: null,
+ SkipDirectorys: null
+ );
+
+ Assert.Single(config.BlackFiles);
+ Assert.Null(config.BlackFormats);
+ Assert.Null(config.SkipDirectorys);
+ }
+
+ #endregion
+
+ #region HubConfig
+
+ [Fact]
+ public void HubConfig_WithUrl_DefaultsReasonable()
+ {
+ var config = new HubConfig { Url = "https://signalr.example.com/hub" };
+
+ Assert.Equal("https://signalr.example.com/hub", config.Url);
+ Assert.Equal(TimeSpan.FromSeconds(5), config.ReconnectDelay);
+ Assert.Equal(10, config.MaxReconnectAttempts);
+ }
+
+ [Fact]
+ public void HubConfig_AllFields_Customized()
+ {
+ var config = new HubConfig
+ {
+ Url = "wss://push.example.com/update-hub",
+ ReconnectDelay = TimeSpan.FromSeconds(10),
+ MaxReconnectAttempts = 20
+ };
+
+ Assert.Equal("wss://push.example.com/update-hub", config.Url);
+ Assert.Equal(TimeSpan.FromSeconds(10), config.ReconnectDelay);
+ Assert.Equal(20, config.MaxReconnectAttempts);
+ }
+
+ #endregion
+
+ #region DownloadAsset
+
+ [Fact]
+ public void DownloadAsset_AllFields_ConstructsCorrectly()
+ {
+ var asset = new DownloadAsset(
+ Name: "client-app-v2.0.0.zip",
+ Url: "https://cdn.example.com/packages/v2.0.0.zip",
+ Size: 100L * 1024 * 1024,
+ SHA256: "sha256:abc123def456",
+ Version: "2.0.0",
+ Priority: DownloadPriority.Normal
+ );
+
+ Assert.Equal("client-app-v2.0.0.zip", asset.Name);
+ Assert.Equal("2.0.0", asset.Version);
+ Assert.Equal("https://cdn.example.com/packages/v2.0.0.zip", asset.Url);
+ Assert.Equal("sha256:abc123def456", asset.SHA256);
+ Assert.Equal(104857600, asset.Size);
+ Assert.Equal(DownloadPriority.Normal, asset.Priority);
+ Assert.False(asset.IsCrossVersion);
+ Assert.False(asset.IsForcibly);
+ Assert.False(asset.IsFreeze);
+ }
+
+ [Fact]
+ public void DownloadAsset_CrossVersion_IsForcibly()
+ {
+ var asset = new DownloadAsset(
+ Name: "critical-patch.zip",
+ Url: "https://cdn.example.com/security/hotfix.zip",
+ Size: 5L * 1024 * 1024,
+ SHA256: null,
+ Version: "2.0.1-hotfix",
+ Priority: DownloadPriority.High,
+ IsForcibly: true,
+ IsCrossVersion: true,
+ FromVersion: "2.0.0"
+ );
+
+ Assert.Equal(DownloadPriority.High, asset.Priority);
+ Assert.True(asset.IsForcibly);
+ Assert.True(asset.IsCrossVersion);
+ Assert.Equal("2.0.0", asset.FromVersion);
+ }
+
+ #endregion
+
+ #region DownloadPlan
+
+ [Fact]
+ public void DownloadPlan_Empty_HasNoAssets()
+ {
+ var plan = DownloadPlan.Empty;
+ Assert.False(plan.HasAssets);
+ Assert.Empty(plan.Assets);
+ Assert.False(plan.IsForcibly);
+ }
+
+ [Fact]
+ public void DownloadPlan_WithAssets_HasCorrectCount()
+ {
+ var assets = new List
+ {
+ new("update-v2.zip", "https://cdn.example.com/v2.zip", 50 * 1024 * 1024, null, "2.0.0"),
+ new("patch-v2.1.zip", "https://cdn.example.com/v2.1.zip", 5 * 1024 * 1024, null, "2.1.0")
+ };
+ var plan = new DownloadPlan(assets, false);
+
+ Assert.True(plan.HasAssets);
+ Assert.Equal(2, plan.Assets.Count);
+ }
+
+ #endregion
+
+ #region DownloadProgress
+
+ [Fact]
+ public void DownloadProgress_Pending()
+ {
+ var progress = new DownloadProgress("update.zip", 0, 50L * 1024 * 1024, 0.0, DownloadStatus.Pending);
+
+ Assert.Equal(DownloadStatus.Pending, progress.Status);
+ Assert.Equal(0, progress.BytesDownloaded);
+ Assert.Equal(0.0, progress.Percentage);
+ }
+
+ [Fact]
+ public void DownloadProgress_HalfComplete()
+ {
+ var progress = new DownloadProgress("update.zip", 25L * 1024 * 1024, 50L * 1024 * 1024, 50.0, DownloadStatus.Downloading);
+
+ Assert.Equal(DownloadStatus.Downloading, progress.Status);
+ Assert.Equal(50.0, progress.Percentage);
+ }
+
+ [Fact]
+ public void DownloadProgress_Completed()
+ {
+ var progress = new DownloadProgress("update.zip", 50L * 1024 * 1024, 50L * 1024 * 1024, 100.0, DownloadStatus.Completed);
+
+ Assert.Equal(DownloadStatus.Completed, progress.Status);
+ Assert.Equal(100.0, progress.Percentage);
+ }
+
+ [Fact]
+ public void DownloadProgress_Failed()
+ {
+ var progress = new DownloadProgress("update.zip", 10L * 1024 * 1024, 50L * 1024 * 1024, 20.0, DownloadStatus.Failed);
+
+ Assert.Equal(DownloadStatus.Failed, progress.Status);
+ Assert.NotEqual(100.0, progress.Percentage);
+ }
+
+ #endregion
+
+ #region DownloadResult
+
+ [Fact]
+ public void DownloadResult_Success()
+ {
+ var result = new DownloadResult(
+ "https://cdn.example.com/update.zip",
+ "/tmp/update.zip",
+ 50L * 1024 * 1024,
+ TimeSpan.FromSeconds(30),
+ 0,
+ true,
+ null);
+
+ Assert.True(result.Success);
+ Assert.Equal("/tmp/update.zip", result.LocalPath);
+ Assert.Null(result.ErrorMessage);
+ }
+
+ [Fact]
+ public void DownloadResult_FailureWithRetries()
+ {
+ var result = new DownloadResult(
+ "https://cdn.example.com/update.zip",
+ null,
+ 0,
+ TimeSpan.FromSeconds(15),
+ 3,
+ false,
+ "Connection timeout after 3 retries");
+
+ Assert.False(result.Success);
+ Assert.Equal(3, result.RetryCount);
+ Assert.Equal("Connection timeout after 3 retries", result.ErrorMessage);
+ }
+
+ #endregion
+
+ #region Enum Types
+
+ [Fact]
+ public void AppType_ClientIs1() => Assert.Equal(1, (int)AppType.Client);
+ [Fact]
+ public void AppType_UpgradeIs2() => Assert.Equal(2, (int)AppType.Upgrade);
+ [Fact]
+ public void AppType_OSSIs3() => Assert.Equal(3, (int)AppType.OSS);
+
+ [Fact]
+ public void DiffMode_SerialAndParallel_AreDefined()
+ {
+ Assert.Contains(DiffMode.Serial, Enum.GetValues());
+ Assert.Contains(DiffMode.Parallel, Enum.GetValues());
+ }
+
+ [Fact]
+ public void UpdateMode_HasDefaultAndScripts()
+ {
+ var values = Enum.GetValues();
+ Assert.Contains(UpdateMode.Default, values);
+ Assert.Contains(UpdateMode.Scripts, values);
+ }
+
+ [Fact]
+ public void PlatformType_AllFour()
+ {
+ var values = Enum.GetValues();
+ Assert.Contains(PlatformType.Windows, values);
+ Assert.Contains(PlatformType.Linux, values);
+ Assert.Contains(PlatformType.MacOS, values);
+ Assert.Contains(PlatformType.Unknown, values);
+ }
+
+ [Fact]
+ public void OssProvider_AllFour()
+ {
+ var values = Enum.GetValues();
+ Assert.Contains(OssProvider.AliYun, values);
+ Assert.Contains(OssProvider.AWS, values);
+ Assert.Contains(OssProvider.MinIO, values);
+ Assert.Contains(OssProvider.Tencent, values);
+ }
+
+ [Fact]
+ public void DownloadStatus_FiveValues()
+ {
+ var values = Enum.GetValues();
+ Assert.Contains(DownloadStatus.Pending, values);
+ Assert.Contains(DownloadStatus.Downloading, values);
+ Assert.Contains(DownloadStatus.Completed, values);
+ Assert.Contains(DownloadStatus.Failed, values);
+ Assert.Contains(DownloadStatus.Retrying, values);
+ }
+
+ [Fact]
+ public void DownloadPriority_ThreeValues()
+ {
+ var values = Enum.GetValues();
+ Assert.Contains(DownloadPriority.Low, values);
+ Assert.Contains(DownloadPriority.Normal, values);
+ Assert.Contains(DownloadPriority.High, values);
+ }
+
+ [Fact]
+ public void UpdateEvent_FiveValues()
+ {
+ var values = Enum.GetValues();
+ Assert.Contains(UpdateEvent.UpdateStarted, values);
+ Assert.Contains(UpdateEvent.DownloadCompleted, values);
+ Assert.Contains(UpdateEvent.UpdateApplied, values);
+ Assert.Contains(UpdateEvent.UpdateFailed, values);
+ Assert.Contains(UpdateEvent.AppStarted, values);
+ }
+
+ #endregion
+
+ #region UpdateOption Semantics
+
+ [Fact]
+ public void UpdateOption_ValueOf_String_DefaultsCorrectly()
+ {
+ var option = UpdateOption.ValueOf("STRING_KEY", "hello");
+ Assert.Equal("STRING_KEY", option.Name);
+ Assert.Equal("hello", option.DefaultValue);
+ }
+
+ [Fact]
+ public void UpdateOption_ValueOf_Int_DefaultsCorrectly()
+ {
+ var option = UpdateOption.ValueOf("INT_KEY", 42);
+ Assert.Equal(42, option.DefaultValue);
+ }
+
+ [Fact]
+ public void UpdateOption_ValueOf_Bool_DefaultsCorrectly()
+ {
+ var option = UpdateOption.ValueOf("BOOL_KEY", true);
+ Assert.True(option.DefaultValue);
+ }
+
+ [Fact]
+ public void UpdateOption_ValueOf_NullableInt_DefaultsCorrectly()
+ {
+ var option = UpdateOption.ValueOf("NULLABLE_KEY", null);
+ Assert.Null(option.DefaultValue);
+ }
+
+ [Fact]
+ public void UpdateOption_ValueOf_Enum_DefaultsCorrectly()
+ {
+ var option = UpdateOption.ValueOf("ENUM_KEY", AppType.Client);
+ Assert.Equal(AppType.Client, option.DefaultValue);
+ }
+
+ #endregion
+ }
+}