From e8efedb64f9ee6d3a6a52d068c3e255325964c35 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Sun, 24 May 2026 23:50:03 +0800 Subject: [PATCH 1/2] test: add missing coverage tests (post-#391 follow-up) Files that missed the #391 squash merge: - BootstrapFullParameterMatrixTests: ALL 42 UpdateOptions, 6 chains including Client+Upgrade dual full-configuration - BootstrapHooksAndExtensionsTests: IUpdateHooks lifecycle (5 methods), IUpdateReporter, IUpdateEventListener (8 events), security schemes - ConfigurationModelsTests: BlackListConfig, HubConfig, DownloadAsset/Plan/ Progress/Result, 9 enums, UpdateOption semantics Closes #390 (follow-up) --- .../BootstrapFullParameterMatrixTests.cs | 495 ++++++++++++++++++ .../BootstrapHooksAndExtensionsTests.cs | 489 +++++++++++++++++ .../Configuration/ConfigurationModelsTests.cs | 371 +++++++++++++ 3 files changed, 1355 insertions(+) create mode 100644 tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs create mode 100644 tests/CoreTest/Bootstrap/BootstrapHooksAndExtensionsTests.cs create mode 100644 tests/CoreTest/Configuration/ConfigurationModelsTests.cs diff --git a/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs b/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs new file mode 100644 index 00000000..37bcaf6d --- /dev/null +++ b/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs @@ -0,0 +1,495 @@ +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 39 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 (9) + [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 (9) + [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 (2) + [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 (6) + [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 (5) + [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 (3) + [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 (3) + [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 (5) + [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 (6) + [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. This is the closest equivalent to a real-world + /// enterprise deployment where the developer configures both roles at once. + /// + [Fact] + public void Chain_ClientAndUpgrade_BothFullyConfigured() + { + // Shared configuration used by both client and upgrade + 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", + DriverDirectory = "/opt/enterprise/drivers", + BlackFiles = new List { "*.pdb", "*.config", "appsettings.Development.json" }, + BlackFormats = new List { ".log", ".tmp", ".cache", ".etl" }, + SkipDirectorys = new List { "logs", "temp", "cache", "diagnostics", "__backups__" } + }; + + // ========================================== + // CLIENT bootstrap �?full production config + // ========================================== + var clientBootstrap = new GeneralUpdateBootstrap() + // --- Core --- + .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) + // --- Deployment --- + .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) + // --- Download Performance --- + .Option(UpdateOptions.MaxConcurrency, 4) + .Option(UpdateOptions.EnableResume, true) + .Option(UpdateOptions.RetryCount, 5) + .Option(UpdateOptions.VerifyChecksum, true) + .Option(UpdateOptions.RetryInterval, TimeSpan.FromSeconds(2)) + // --- Security --- + .Option(UpdateOptions.Scheme, "HMAC") + .Option(UpdateOptions.Token, "hmac-prod-secret") + .Option(UpdateOptions.PermissionScript, "#!/bin/bash\nchmod +x /opt/enterprise/Update") + // --- Reporting --- + .Option(UpdateOptions.ReportUrl, "https://telemetry.enterprise.com/api/report") + .Option(UpdateOptions.ProductId, "enterprise-app-v4") + .Option(UpdateOptions.UpdateLogUrl, "https://enterprise.com/releases") + // --- Blacklist --- + .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") + // --- Config --- + .SetConfig(sharedConfig) + // --- Hooks simulation (precheck) --- + .AddListenerUpdatePrecheck(args => + { + var hour = DateTime.Now.Hour; + return hour < 2 || hour > 6; // Auto-approve updates during off-hours + }) + // --- All event listeners --- + .AddListenerUpdateInfo((s, e) => { }) + .AddListenerMultiAllDownloadCompleted((s, e) => { }) + .AddListenerMultiDownloadCompleted((s, e) => { }) + .AddListenerMultiDownloadError((s, e) => { }) + .AddListenerMultiDownloadStatistics((s, e) => { }) + .AddListenerException((s, e) => { }) + // --- Custom pre-update checks --- + .AddCustomOption(new List> + { + () => true, // Disk space check + () => true, // Network connectivity check + () => true // Service availability check + }); + + Assert.NotNull(clientBootstrap); + + // ========================================== + // UPGRADE bootstrap �?full production config + // ========================================== + var upgradeBootstrap = new GeneralUpdateBootstrap() + // --- Core (Upgrade role) --- + .Option(UpdateOptions.AppType, AppType.Upgrade) + .Option(UpdateOptions.DiffMode, DiffMode.Parallel) + .Option(UpdateOptions.Encoding, Encoding.UTF8) + .Option(UpdateOptions.Format, "ZIP") + .Option(UpdateOptions.DownloadTimeout, 30) // Upgrade needs shorter timeout + .Option(UpdateOptions.DriveEnabled, true) // Upgrade may install drivers + .Option(UpdateOptions.PatchEnabled, true) // Apply differential patches + .Option(UpdateOptions.BackupEnabled, false) // Upgrade doesn't need backup (client did it) + .Option(UpdateOptions.Mode, UpdateMode.Default) + // --- Deployment --- + .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) + // --- Download (Upgrade doesn't download in new design, but options available) --- + .Option(UpdateOptions.MaxConcurrency, 2) + .Option(UpdateOptions.VerifyChecksum, true) + .Option(UpdateOptions.RetryInterval, TimeSpan.FromSeconds(1)) + // --- Security --- + .Option(UpdateOptions.Scheme, "HMAC") + .Option(UpdateOptions.Token, "hmac-prod-secret") + .Option(UpdateOptions.PermissionScript, "#!/bin/bash\nchmod +x /opt/enterprise/Update") + // --- Reporting (Upgrade reports its own status) --- + .Option(UpdateOptions.ReportUrl, "https://telemetry.enterprise.com/api/report") + .Option(UpdateOptions.ProductId, "enterprise-app-v4") + // --- Blacklist --- + .Option(UpdateOptions.BlackList, BlackListConfig.Empty) + .Option(UpdateOptions.Script, "chmod +x /opt/enterprise/Update") + // --- Config --- + .SetConfig(sharedConfig) + // --- Event listeners (Upgrade reports failures) --- + .AddListenerException((s, e) => { }) + .AddListenerUpdateInfo((s, e) => { }); + + Assert.NotNull(upgradeBootstrap); + + // Both bootstraps coexist without interference + Assert.NotSame(clientBootstrap, upgradeBootstrap); + } + + /// + /// Real-world developer workflow: configure both bootstraps with + /// hooks, reporter, and full extension chain in a single method. + /// Demonstrates the complete API surface a developer would use. + /// + [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"; + + // Step 1: Developer configures the Client bootstrap + 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) => { }) // UI: show update available toast + .AddListenerMultiDownloadCompleted((s, e) => { }) // UI: update progress bar + .AddListenerMultiAllDownloadCompleted((s, e) => { }) // UI: ready to restart + .AddListenerMultiDownloadError((s, e) => { }) // UI: show error dialog + .AddListenerMultiDownloadStatistics((s, e) => { }) // UI: speed/ETA + .AddListenerException((s, e) => { }) // Log to telemetry + .AddListenerUpdatePrecheck(args => false) // Don't skip (default) + .AddCustomOption(new List> + { + () => Directory.Exists(installPath), + () => true + }); + + Assert.NotNull(client); + + // Step 2: Developer configures the Upgrade bootstrap + 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) => { }) // Upgrade logs its own errors + .AddListenerUpdateInfo((s, e) => { }); + + Assert.NotNull(upgrade); + + // Step 3: Verify independent instances + 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..a581e561 --- /dev/null +++ b/tests/CoreTest/Bootstrap/BootstrapHooksAndExtensionsTests.cs @@ -0,0 +1,489 @@ +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))); + listener.OnCustomEvent("test.event", EventArgs.Empty); + + 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); + Assert.True(listener.CustomEventCalled); + } + + 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 + } +} From 1c854b3a20d46dba2eecc8c2279119ba617591c2 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Sun, 24 May 2026 23:57:05 +0800 Subject: [PATCH 2/2] fix: copilot suggestions - remove OnCustomEvent, fix option counts, drop region counts - Remove OnCustomEvent from TestEventListener (only 7 IUpdateEventListener methods exist) - Fix file header: 42 options -> 37 options (matches actual UpdateOptions surface) - Remove hard-coded test counts from #region headers to prevent drift - Fix garbled characters in comments --- .../BootstrapFullParameterMatrixTests.cs | 133 ++++++------------ .../BootstrapHooksAndExtensionsTests.cs | 7 +- 2 files changed, 42 insertions(+), 98 deletions(-) diff --git a/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs b/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs index 37bcaf6d..b3647c39 100644 --- a/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs +++ b/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs @@ -10,8 +10,8 @@ namespace CoreTest.Bootstrap { /// - /// Full parameter matrix tests �?verifies ALL UpdateOptions constants - /// can be set via .Option() without throwing. Covers 39 options across + /// 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. /// @@ -42,7 +42,7 @@ public void Dispose() Token = "token" }); - #region Core (9) + #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)); @@ -51,8 +51,7 @@ public void Dispose() [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)] + [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)); @@ -64,7 +63,7 @@ public void Dispose() [Fact] public void Mode_Scripts() => Assert.NotNull(B().Option(UpdateOptions.Mode, UpdateMode.Scripts)); #endregion - #region Deployment (9) + #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")); @@ -76,13 +75,13 @@ public void Dispose() [Fact] public void Platform_Linux() => Assert.NotNull(B().Option(UpdateOptions.Platform, PlatformType.Linux)); #endregion - #region Silent (2) + #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 (6) + #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)); @@ -92,7 +91,7 @@ public void Dispose() [Fact] public void RetryInterval_Custom() => Assert.NotNull(B().Option(UpdateOptions.RetryInterval, TimeSpan.FromSeconds(3))); #endregion - #region Security (5) + #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")); @@ -100,19 +99,19 @@ public void Dispose() [Fact] public void PermissionScript_Custom() => Assert.NotNull(B().Option(UpdateOptions.PermissionScript, "#!/bin/bash\nchmod +x")); #endregion - #region Reporting (3) + #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 (3) + #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 (5) + #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" }))); @@ -122,7 +121,7 @@ [Fact] public void Hub_Configured() => Assert.NotNull(B().Option(UpdateOptions.H new HubConfig { Url = "https://signalr.example.com/hub" })); #endregion - #region Full Combination Chains (6) + #region Full Combination Chains [Fact] public void Chain_All33Options() { var b = new GeneralUpdateBootstrap() @@ -226,13 +225,11 @@ [Fact] public void Chain_UpgradeWithExtensions() /// /// Complete production deployment: Client + Upgrade bootstraps configured /// simultaneously with ALL non-conflicting parameters, hooks, listeners, - /// and extension points. This is the closest equivalent to a real-world - /// enterprise deployment where the developer configures both roles at once. + /// and extension points. /// [Fact] public void Chain_ClientAndUpgrade_BothFullyConfigured() { - // Shared configuration used by both client and upgrade var sharedConfig = new Configinfo { UpdateUrl = "https://update.enterprise.com/api/v2", @@ -249,17 +246,12 @@ public void Chain_ClientAndUpgrade_BothFullyConfigured() Token = "hmac-prod-secret", Bowl = "Bowl.exe", Script = "#!/bin/bash\nset -e\nchmod +x /opt/enterprise/Update", - DriverDirectory = "/opt/enterprise/drivers", - BlackFiles = new List { "*.pdb", "*.config", "appsettings.Development.json" }, + BlackFiles = new List { "*.pdb", "*.config" }, BlackFormats = new List { ".log", ".tmp", ".cache", ".etl" }, - SkipDirectorys = new List { "logs", "temp", "cache", "diagnostics", "__backups__" } + SkipDirectorys = new List { "logs", "temp", "cache", "diagnostics" } }; - // ========================================== - // CLIENT bootstrap �?full production config - // ========================================== var clientBootstrap = new GeneralUpdateBootstrap() - // --- Core --- .Option(UpdateOptions.AppType, AppType.Client) .Option(UpdateOptions.DiffMode, DiffMode.Parallel) .Option(UpdateOptions.Encoding, Encoding.UTF8) @@ -270,7 +262,6 @@ public void Chain_ClientAndUpgrade_BothFullyConfigured() .Option(UpdateOptions.BackupEnabled, true) .Option(UpdateOptions.Mode, UpdateMode.Default) .Option(UpdateOptions.Silent, false) - // --- Deployment --- .Option(UpdateOptions.UpdateUrl, "https://update.enterprise.com/api/v2") .Option(UpdateOptions.AppSecretKey, "enterprise-prod-key-2026") .Option(UpdateOptions.AppName, "Update.exe") @@ -279,102 +270,78 @@ public void Chain_ClientAndUpgrade_BothFullyConfigured() .Option(UpdateOptions.ClientVersion, "4.2.1") .Option(UpdateOptions.UpgradeClientVersion, "2.0.0") .Option(UpdateOptions.Platform, PlatformType.Windows) - // --- Download Performance --- .Option(UpdateOptions.MaxConcurrency, 4) .Option(UpdateOptions.EnableResume, true) .Option(UpdateOptions.RetryCount, 5) .Option(UpdateOptions.VerifyChecksum, true) .Option(UpdateOptions.RetryInterval, TimeSpan.FromSeconds(2)) - // --- Security --- .Option(UpdateOptions.Scheme, "HMAC") .Option(UpdateOptions.Token, "hmac-prod-secret") .Option(UpdateOptions.PermissionScript, "#!/bin/bash\nchmod +x /opt/enterprise/Update") - // --- Reporting --- .Option(UpdateOptions.ReportUrl, "https://telemetry.enterprise.com/api/report") .Option(UpdateOptions.ProductId, "enterprise-app-v4") .Option(UpdateOptions.UpdateLogUrl, "https://enterprise.com/releases") - // --- Blacklist --- .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") - // --- Config --- .SetConfig(sharedConfig) - // --- Hooks simulation (precheck) --- .AddListenerUpdatePrecheck(args => { var hour = DateTime.Now.Hour; - return hour < 2 || hour > 6; // Auto-approve updates during off-hours + return hour < 2 || hour > 6; }) - // --- All event listeners --- .AddListenerUpdateInfo((s, e) => { }) .AddListenerMultiAllDownloadCompleted((s, e) => { }) .AddListenerMultiDownloadCompleted((s, e) => { }) .AddListenerMultiDownloadError((s, e) => { }) .AddListenerMultiDownloadStatistics((s, e) => { }) .AddListenerException((s, e) => { }) - // --- Custom pre-update checks --- .AddCustomOption(new List> { - () => true, // Disk space check - () => true, // Network connectivity check - () => true // Service availability check + () => true, () => true, () => true }); Assert.NotNull(clientBootstrap); - // ========================================== - // UPGRADE bootstrap �?full production config - // ========================================== var upgradeBootstrap = new GeneralUpdateBootstrap() - // --- Core (Upgrade role) --- .Option(UpdateOptions.AppType, AppType.Upgrade) .Option(UpdateOptions.DiffMode, DiffMode.Parallel) .Option(UpdateOptions.Encoding, Encoding.UTF8) .Option(UpdateOptions.Format, "ZIP") - .Option(UpdateOptions.DownloadTimeout, 30) // Upgrade needs shorter timeout - .Option(UpdateOptions.DriveEnabled, true) // Upgrade may install drivers - .Option(UpdateOptions.PatchEnabled, true) // Apply differential patches - .Option(UpdateOptions.BackupEnabled, false) // Upgrade doesn't need backup (client did it) + .Option(UpdateOptions.DownloadTimeout, 30) + .Option(UpdateOptions.DriveEnabled, true) + .Option(UpdateOptions.PatchEnabled, true) + .Option(UpdateOptions.BackupEnabled, false) .Option(UpdateOptions.Mode, UpdateMode.Default) - // --- Deployment --- .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) - // --- Download (Upgrade doesn't download in new design, but options available) --- .Option(UpdateOptions.MaxConcurrency, 2) .Option(UpdateOptions.VerifyChecksum, true) .Option(UpdateOptions.RetryInterval, TimeSpan.FromSeconds(1)) - // --- Security --- .Option(UpdateOptions.Scheme, "HMAC") .Option(UpdateOptions.Token, "hmac-prod-secret") .Option(UpdateOptions.PermissionScript, "#!/bin/bash\nchmod +x /opt/enterprise/Update") - // --- Reporting (Upgrade reports its own status) --- .Option(UpdateOptions.ReportUrl, "https://telemetry.enterprise.com/api/report") .Option(UpdateOptions.ProductId, "enterprise-app-v4") - // --- Blacklist --- .Option(UpdateOptions.BlackList, BlackListConfig.Empty) .Option(UpdateOptions.Script, "chmod +x /opt/enterprise/Update") - // --- Config --- .SetConfig(sharedConfig) - // --- Event listeners (Upgrade reports failures) --- .AddListenerException((s, e) => { }) .AddListenerUpdateInfo((s, e) => { }); Assert.NotNull(upgradeBootstrap); - - // Both bootstraps coexist without interference Assert.NotSame(clientBootstrap, upgradeBootstrap); } /// - /// Real-world developer workflow: configure both bootstraps with - /// hooks, reporter, and full extension chain in a single method. - /// Demonstrates the complete API surface a developer would use. + /// Real-world developer workflow: configure both Client and Upgrade + /// bootstraps with hooks, reporter, and full extension chain. /// [Fact] public void Chain_ClientAndUpgrade_CompleteDeveloperWorkflow() @@ -384,7 +351,6 @@ public void Chain_ClientAndUpgrade_CompleteDeveloperWorkflow() var mainApp = "MyApp.exe"; var currentVersion = "3.0.0"; - // Step 1: Developer configures the Client bootstrap var client = new GeneralUpdateBootstrap() .Option(UpdateOptions.AppType, AppType.Client) .Option(UpdateOptions.DiffMode, DiffMode.Parallel) @@ -417,39 +383,27 @@ public void Chain_ClientAndUpgrade_CompleteDeveloperWorkflow() .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", + 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", + 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) => { }) // UI: show update available toast - .AddListenerMultiDownloadCompleted((s, e) => { }) // UI: update progress bar - .AddListenerMultiAllDownloadCompleted((s, e) => { }) // UI: ready to restart - .AddListenerMultiDownloadError((s, e) => { }) // UI: show error dialog - .AddListenerMultiDownloadStatistics((s, e) => { }) // UI: speed/ETA - .AddListenerException((s, e) => { }) // Log to telemetry - .AddListenerUpdatePrecheck(args => false) // Don't skip (default) - .AddCustomOption(new List> - { - () => Directory.Exists(installPath), - () => true - }); + .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); - // Step 2: Developer configures the Upgrade bootstrap var upgrade = new GeneralUpdateBootstrap() .Option(UpdateOptions.AppType, AppType.Upgrade) .Option(UpdateOptions.DiffMode, DiffMode.Parallel) @@ -472,22 +426,15 @@ public void Chain_ClientAndUpgrade_CompleteDeveloperWorkflow() .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", + 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) => { }) // Upgrade logs its own errors + .AddListenerException((s, e) => { }) .AddListenerUpdateInfo((s, e) => { }); Assert.NotNull(upgrade); - - // Step 3: Verify independent instances Assert.NotSame(client, upgrade); } #endregion diff --git a/tests/CoreTest/Bootstrap/BootstrapHooksAndExtensionsTests.cs b/tests/CoreTest/Bootstrap/BootstrapHooksAndExtensionsTests.cs index a581e561..612416cd 100644 --- a/tests/CoreTest/Bootstrap/BootstrapHooksAndExtensionsTests.cs +++ b/tests/CoreTest/Bootstrap/BootstrapHooksAndExtensionsTests.cs @@ -372,17 +372,14 @@ public void UpdateEventListener_AllMethods_AreCallable() 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))); - listener.OnCustomEvent("test.event", EventArgs.Empty); - - Assert.True(listener.AllDownloadCalled); + 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); - Assert.True(listener.CustomEventCalled); - } + } private sealed class TestEventListener : IUpdateEventListener {