diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 496144b8..8e547669 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,8 +29,6 @@ jobs: - name: Test (Windows) if: runner.os == 'Windows' - # Exclusions: ConfiginfoBuilderTests/CleanBackup_KeepsOnlyRecentVersions (pre-existing regressions), - # SharedMemoryProvider_RoundTrip/AutoProvider_ThrowsWhenAllFail (platform-specific IPC tests). run: dotnet test ./src/c#/GeneralUpdate.slnx -c Release --no-build --filter "FullyQualifiedName!~ConfiginfoBuilderTests&FullyQualifiedName!~CleanBackup_KeepsOnlyRecentVersions&FullyQualifiedName!~SharedMemoryProvider_RoundTrip&FullyQualifiedName!~AutoProvider_ThrowsWhenAllFail" - name: Test (Ubuntu - cross-platform) @@ -38,7 +36,6 @@ jobs: run: | dotnet test tests/CoreTest/CoreTest.csproj -c Release --no-build --filter "FullyQualifiedName!~ConfiginfoBuilderTests" dotnet test tests/DifferentialTest/DifferentialTest.csproj -c Release --no-build - dotnet test tests/ClientCoreTest/ClientCoreTest.csproj -c Release --no-build aot-verify: runs-on: windows-latest @@ -54,8 +51,5 @@ jobs: - name: Restore run: dotnet restore ./src/c#/GeneralUpdate.slnx - # Verify AOT compatibility via trim analyzer warnings. - # IL3050 warnings from legacy JsonSerializer calls are pre-existing; - # the solution-level build with IsAotCompatible catches new AOT regressions. - name: Verify AOT compatibility run: dotnet build ./src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj -c Release -f net10.0 /p:IsAotCompatible=true --no-restore diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index 3f333401..9435231e 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -37,7 +37,6 @@ public class GeneralUpdateBootstrap : AbstractBootstrap? _customSkipOption; private Func? _updatePrecheck; - private readonly List> _customOptions = new(); private CancellationTokenSource? _cts; public GeneralUpdateBootstrap() @@ -105,7 +104,7 @@ private async Task LaunchWithStrategy(IStrategy roleStra if (hubConfig != null && !string.IsNullOrEmpty(hubConfig.Url)) { var hubSource = new Download.Sources.HubDownloadSource( - hubConfig.Url, GetOption(UpdateOptions.Token), GetOption(UpdateOptions.AppSecretKey)); + hubConfig.Url, _configInfo.Token, _configInfo.AppSecretKey); await hubSource.StartAsync().ConfigureAwait(false); resolvedSource = hubSource; GeneralTracer.Info("GeneralUpdateBootstrap: HubDownloadSource started from HubConfig."); @@ -115,8 +114,6 @@ private async Task LaunchWithStrategy(IStrategy roleStra clientStrat.DownloadSource = resolvedSource; if (_updatePrecheck != null) clientStrat.UseUpdatePrecheck(_updatePrecheck); - foreach (var opt in _customOptions) - clientStrat.UseCustomOption(opt); await CallSmallBowlHomeAsync(_configInfo.Bowl).ConfigureAwait(false); } else if (roleStrategy is UpgradeUpdateStrategy upgradeStrat) @@ -244,13 +241,6 @@ public GeneralUpdateBootstrap AddListenerUpdatePrecheck(Func> funcList) - { - Debug.Assert(funcList != null && funcList.Any()); - _customOptions.AddRange(funcList); - return this; - } - // ════════════════════════════════════════════════════════════════ // Helpers // ════════════════════════════════════════════════════════════════ diff --git a/src/c#/GeneralUpdate.Core/Configuration/DiffMode.cs b/src/c#/GeneralUpdate.Core/Configuration/DiffMode.cs index 7dca9aec..93185ddd 100644 --- a/src/c#/GeneralUpdate.Core/Configuration/DiffMode.cs +++ b/src/c#/GeneralUpdate.Core/Configuration/DiffMode.cs @@ -1,3 +1,12 @@ namespace GeneralUpdate.Core.Configuration; -public enum DiffMode { Serial, Parallel } +/// +/// Diff/patch generation mode. +/// +public enum DiffMode +{ + /// Process diffs one file at a time. + Serial, + /// Process diffs in parallel for faster throughput. + Parallel +} diff --git a/src/c#/GeneralUpdate.Core/Configuration/Format.cs b/src/c#/GeneralUpdate.Core/Configuration/Format.cs index 1c8e98dd..b46115d5 100644 --- a/src/c#/GeneralUpdate.Core/Configuration/Format.cs +++ b/src/c#/GeneralUpdate.Core/Configuration/Format.cs @@ -1,7 +1,11 @@ namespace GeneralUpdate.Core.Configuration { + /// + /// Compression format constants for update packages. + /// public class Format { + /// ZIP compression format extension. public const string ZIP = ".zip"; } } \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Core/Configuration/ReportType.cs b/src/c#/GeneralUpdate.Core/Configuration/ReportType.cs index 29cc1a80..e493f53c 100644 --- a/src/c#/GeneralUpdate.Core/Configuration/ReportType.cs +++ b/src/c#/GeneralUpdate.Core/Configuration/ReportType.cs @@ -1,10 +1,16 @@ namespace GeneralUpdate.Core.Configuration; +/// +/// Report status type constants for update operation results. +/// public class ReportType { + /// No report / default state. public const int None = 0; + /// Update succeeded. public const int Success = 2; + /// Update failed. public const int Failure = 3; -} \ No newline at end of file +} diff --git a/src/c#/GeneralUpdate.Core/Configuration/UpdateMode.cs b/src/c#/GeneralUpdate.Core/Configuration/UpdateMode.cs index 2d4d2e30..b60eba27 100644 --- a/src/c#/GeneralUpdate.Core/Configuration/UpdateMode.cs +++ b/src/c#/GeneralUpdate.Core/Configuration/UpdateMode.cs @@ -1,7 +1,12 @@ namespace GeneralUpdate.Core.Configuration; +/// +/// Specifies the deployment mode for updates. +/// public enum UpdateMode { + /// Standard file-based update. Default = 0, + /// Script-based custom update logic. Scripts = 1 } \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Core/Configuration/UpdateOptions.cs b/src/c#/GeneralUpdate.Core/Configuration/UpdateOptions.cs index 91845f38..f4fce946 100644 --- a/src/c#/GeneralUpdate.Core/Configuration/UpdateOptions.cs +++ b/src/c#/GeneralUpdate.Core/Configuration/UpdateOptions.cs @@ -1,74 +1,84 @@ using System; using System.Text; -using GeneralUpdate.Core.FileSystem; namespace GeneralUpdate.Core.Configuration { /// - /// Convenience accessor for UpdateOption constants. + /// Framework-level update option constants. /// Each option has a unique string name and a reasonable default value. - /// Use via .Option(UpdateOptions.UpdateUrl, "https://..."). + /// Business-specific configuration (URLs, keys, app names, etc.) belongs in + /// / . /// public static class UpdateOptions { // ═══ Core ═══ + /// Application role type — Client, Upgrade, or OSS. public static UpdateOption AppType { get; } = UpdateOption.ValueOf("APPTYPE", Configuration.AppType.Client); // ═══ Diff mode ═══ + /// Diff/patch generation mode — Serial or Parallel. public static UpdateOption DiffMode { get; } = UpdateOption.ValueOf("DIFFMODE", Configuration.DiffMode.Serial); // ═══ Backward-compatible options ═══ + /// Compression encoding for update packages. public static UpdateOption Encoding { get; } = UpdateOption.ValueOf("COMPRESSENCODING", System.Text.Encoding.UTF8); + + /// Compression format (e.g., "ZIP"). public static UpdateOption Format { get; } = UpdateOption.ValueOf("COMPRESSFORMAT", "ZIP"); + + /// Download timeout in seconds. public static UpdateOption DownloadTimeout { get; } = UpdateOption.ValueOf("DOWNLOADTIMEOUT", 30); + + /// Whether driver update mode is enabled. public static UpdateOption DriveEnabled { get; } = UpdateOption.ValueOf("DRIVE", false); + + /// Whether differential patch update is enabled. public static UpdateOption PatchEnabled { get; } = UpdateOption.ValueOf("PATCH", true); + + /// Whether backup before update is enabled. public static UpdateOption BackupEnabled { get; } = UpdateOption.ValueOf("BACKUP", true); + + /// Update mode override. public static UpdateOption Mode { get; } = UpdateOption.ValueOf("MODE", null); + + /// Whether silent background update is enabled. public static UpdateOption Silent { get; } = UpdateOption.ValueOf("ENABLESILENTUPDATE", false); - // ═══ New options ═══ - public static UpdateOption UpdateUrl { get; } = UpdateOption.ValueOf("UPDATEURL", null); - public static UpdateOption AppSecretKey { get; } = UpdateOption.ValueOf("APPSECRETKEY", string.Empty); - public static UpdateOption AppName { get; } = UpdateOption.ValueOf("APPNAME", string.Empty); - public static UpdateOption MainAppName { get; } = UpdateOption.ValueOf("MAINAPPNAME", string.Empty); - public static UpdateOption InstallPath { get; } = UpdateOption.ValueOf("INSTALLPATH", AppContext.BaseDirectory); - public static UpdateOption ClientVersion { get; } = UpdateOption.ValueOf("CLIENTVERSION", string.Empty); - public static UpdateOption UpgradeClientVersion { get; } = UpdateOption.ValueOf("UPGRADECLIENTVERSION", null); - public static UpdateOption Platform { get; } = UpdateOption.ValueOf("PLATFORM", null); + // ═══ Silent mode ═══ + /// Whether silent updates auto-install without user intervention. public static UpdateOption SilentAutoInstall { get; } = UpdateOption.ValueOf("SILENTAUTOINSTALL", false); + + /// Polling interval in minutes for silent update checks. public static UpdateOption SilentPollIntervalMinutes { get; } = UpdateOption.ValueOf("SILENTPOLLINTERVALMINUTES", 60); + + // ═══ Concurrency & Resume ═══ + /// Maximum concurrent download operations. public static UpdateOption MaxConcurrency { get; } = UpdateOption.ValueOf("MAXCONCURRENCY", 3); + + /// Whether download resume is enabled. public static UpdateOption EnableResume { get; } = UpdateOption.ValueOf("ENABLERESUME", true); + + // ═══ Resilience ═══ + /// Number of retry attempts for failed operations. public static UpdateOption RetryCount { get; } = UpdateOption.ValueOf("RETRYCOUNT", 3); + + /// Whether checksum verification is performed after download. public static UpdateOption VerifyChecksum { get; } = UpdateOption.ValueOf("VERIFYCHECKSUM", true); - public static UpdateOption ReportUrl { get; } = UpdateOption.ValueOf("REPORTURL", null); - public static UpdateOption ProductId { get; } = UpdateOption.ValueOf("PRODUCTID", null); - public static UpdateOption PermissionScript { get; } = UpdateOption.ValueOf("PERMISSIONSCRIPT", null); - public static UpdateOption Scheme { get; } = UpdateOption.ValueOf("SCHEME", null); - public static UpdateOption Token { get; } = UpdateOption.ValueOf("TOKEN", null); + + /// Initial retry interval for exponential back-off. Default 1 second. + public static UpdateOption RetryInterval { get; } = UpdateOption.ValueOf("RETRYINTERVAL", TimeSpan.FromSeconds(1)); // ═══ OSS ═══ + /// Object Storage Service provider type. public static UpdateOption OSSProvider { get; } = UpdateOption.ValueOf("OSSPROVIDER", null); + + /// OSS bucket region identifier. public static UpdateOption OSSBucketRegion { get; } = UpdateOption.ValueOf("OSSBUCKETREGION", null); // ═══ Blacklist ═══ + /// Blacklist configuration for files and directories to exclude from updates. public static UpdateOption BlackList { get; } = UpdateOption.ValueOf("BLACKLIST", BlackListConfig.Empty); - // ═══ Watchdog ═══ - /// Bowl (crash monitor / watchdog) executable path. - public static UpdateOption Bowl { get; } = UpdateOption.ValueOf("BOWL", null); - - // ═══ Logging & Script ═══ - /// Remote update log / changelog URL. - public static UpdateOption UpdateLogUrl { get; } = UpdateOption.ValueOf("UPDATELOGURL", null); - /// Custom execution script path for pre/post-update actions. - public static UpdateOption Script { get; } = UpdateOption.ValueOf("SCRIPT", null); - - // ═══ Retry ═══ - /// Initial retry interval for exponential backoff. Default 1 second. - public static UpdateOption RetryInterval { get; } = UpdateOption.ValueOf("RETRYINTERVAL", TimeSpan.FromSeconds(1)); - // ═══ SignalR Hub ═══ /// SignalR Hub configuration for push-based updates. public static UpdateOption Hub { get; } = UpdateOption.ValueOf("HUB", null); diff --git a/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs index ceb4aa2b..6daebbdc 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs @@ -6,7 +6,6 @@ using GeneralUpdate.Core.Pipeline; using GeneralUpdate.Core; using GeneralUpdate.Core.Configuration; -using GeneralUpdate.Core.Configuration; using GeneralUpdate.Core.Network; namespace GeneralUpdate.Core.Strategy diff --git a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs index 6805bd67..e34add4a 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs @@ -30,7 +30,6 @@ public class ClientUpdateStrategy : IStrategy private GlobalConfigInfo? _configInfo; private IStrategy? _osStrategy; private Func? _updatePrecheck; - private readonly List> _customOptions = new(); private readonly Download.Abstractions.IDownloadOrchestrator? _orchestrator; private readonly DiffMode _diffMode = DiffMode.Serial; @@ -57,7 +56,6 @@ public async Task ExecuteAsync() { GeneralTracer.Debug("ClientUpdateStrategy.ExecuteAsync start."); await CallSmallBowlHomeAsync(_configInfo.Bowl); - ExecuteCustomOptions(); await ExecuteWorkflowAsync(); } catch (Exception ex) @@ -87,13 +85,6 @@ public ClientUpdateStrategy UseUpdatePrecheck(Func fu return this; } - /// Register custom pre-update operations. - public ClientUpdateStrategy UseCustomOption(Func func) - { - _customOptions.Add(func); - return this; - } - #region Workflow private async Task ExecuteWorkflowAsync() @@ -299,19 +290,6 @@ private async Task CallSmallBowlHomeAsync(string processName) } } - private void ExecuteCustomOptions() - { - foreach (var option in _customOptions) - { - if (!option.Invoke()) - { - var ex = new Exception("Custom option execution failed."); - GeneralTracer.Error("ExecuteCustomOptions failed.", ex); - EventManager.Instance.Dispatch(this, new ExceptionEventArgs(ex, ex.Message)); - } - } - } - // ════════════════════════════════════════════════════════════════ // Hooks & Reporter safe wrappers // ════════════════════════════════════════════════════════════════ diff --git a/src/c#/GeneralUpdate.Core/Strategy/LinuxStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/LinuxStrategy.cs index e9791a96..ee078cc6 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/LinuxStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/LinuxStrategy.cs @@ -1,13 +1,8 @@ using System; using System.Diagnostics; -using System.IO; -using GeneralUpdate.Core.FileSystem; -using GeneralUpdate.Core; -using GeneralUpdate.Core.Event; -using GeneralUpdate.Core.Pipeline; -using GeneralUpdate.Core.Strategy; using GeneralUpdate.Core; using GeneralUpdate.Core.Configuration; +using GeneralUpdate.Core.Event; using GeneralUpdate.Core.Pipeline; namespace GeneralUpdate.Core.Strategy; @@ -59,8 +54,7 @@ public override void StartApp() if (string.IsNullOrEmpty(mainAppPath)) throw new Exception($"Can't find the app {mainAppPath}!"); - GeneralTracer.Info($"GeneralUpdate.Core.LinuxStrategy.StartApp: executing startup script then launching main app={mainAppPath}"); - ExecuteScript(); + GeneralTracer.Info($"GeneralUpdate.Core.LinuxStrategy.StartApp: launching main app={mainAppPath}"); Process.Start(mainAppPath); GeneralTracer.Info("GeneralUpdate.Core.LinuxStrategy.StartApp: main app launched successfully."); } @@ -78,60 +72,4 @@ public override void StartApp() } } - /// - /// Executes the user-specified script. - /// - private void ExecuteScript() - { - try - { - // Check if the script path is valid (_configinfo should come from the base class configuration) - if (string.IsNullOrEmpty(_configinfo.Script) || !File.Exists(_configinfo.Script)) - { - GeneralTracer.Info("No valid script path specified, skipping script execution"); - return; - } - - GeneralTracer.Info($"Starting to execute script: {_configinfo.Script}"); - - // Start process to execute Linux script (using bash) - var processStartInfo = new ProcessStartInfo - { - FileName = "/bin/bash", - Arguments = $"-c \"{_configinfo.Script}\"", // Execute the script - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using var process = Process.Start(processStartInfo); - if (process == null) - { - GeneralTracer.Error("Failed to start script process"); - return; - } - - // Read script output logs - var output = process.StandardOutput.ReadToEnd(); - var error = process.StandardError.ReadToEnd(); - process.WaitForExit(); // Wait for the script to finish execution - - if (!string.IsNullOrEmpty(output)) - GeneralTracer.Info($"Script output: {output}"); - - if (!string.IsNullOrEmpty(error)) - GeneralTracer.Warn($"Script warning: {error}"); - - if (process.ExitCode != 0) - throw new Exception($"Script execution failed, exit code: {process.ExitCode}"); - - GeneralTracer.Info("Script executed successfully"); - } - catch (Exception ex) - { - GeneralTracer.Error("An exception occurred while executing the script", ex); - EventManager.Instance.Dispatch(this, new ExceptionEventArgs(ex, "Script execution failed")); - } - } } diff --git a/src/c#/GeneralUpdate.Core/Strategy/WindowsStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/WindowsStrategy.cs index ac3b4f9f..40809473 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/WindowsStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/WindowsStrategy.cs @@ -4,10 +4,7 @@ using GeneralUpdate.Core; using GeneralUpdate.Core.Event; using GeneralUpdate.Core.Pipeline; -using GeneralUpdate.Core.Strategy; -using GeneralUpdate.Core; using GeneralUpdate.Core.Configuration; -using GeneralUpdate.Core.Pipeline; namespace GeneralUpdate.Core.Strategy { diff --git a/src/c#/GeneralUpdate.slnx b/src/c#/GeneralUpdate.slnx index 57475da9..7ec27ed9 100644 --- a/src/c#/GeneralUpdate.slnx +++ b/src/c#/GeneralUpdate.slnx @@ -9,7 +9,6 @@ - diff --git a/tests/ClientCoreTest/Bootstrap/ClientBootstrapScenarioTests.cs b/tests/ClientCoreTest/Bootstrap/ClientBootstrapScenarioTests.cs deleted file mode 100644 index b8b760db..00000000 --- a/tests/ClientCoreTest/Bootstrap/ClientBootstrapScenarioTests.cs +++ /dev/null @@ -1,681 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using GeneralUpdate.Core; -using GeneralUpdate.Core.Download; -using GeneralUpdate.Core.Configuration; -using GeneralUpdate.Core.Event; -using Xunit; - -namespace ClientCoreTest.Bootstrap -{ - /// - /// Comprehensive ClientBootstrap scenario tests. - /// Covers real-world developer usage patterns: - /// - Client <-> Upgrade mutual upgrade configuration - /// - Version precheck / skip scenarios - /// - Custom option injection - /// - Silent update configuration - /// - Full event listener chain - /// - Push upgrade notification reception - /// - public class ClientBootstrapScenarioTests : IDisposable - { - private readonly string _testDir; - - public ClientBootstrapScenarioTests() - { - _testDir = Path.Combine(Path.GetTempPath(), $"GU_ClientScenario_{Guid.NewGuid()}"); - Directory.CreateDirectory(_testDir); - } - - public void Dispose() - { - try { Directory.Delete(_testDir, true); } catch { /* ignore */ } - } - - #region Client <-> Upgrade Mutual Upgrade - - /// - /// Scenario: Developer sets up client for mutual upgrade. - /// Both client and upgrade versions need checking. - /// - [Fact] - public void MutualUpgrade_BothNeedUpdate_ConfiguresClientCorrectly() - { - // Arrange - client-side developer configuration - var config = new Configinfo - { - UpdateUrl = "https://update.company.com/api", - AppName = "Update.exe", - MainAppName = "ProductApp.exe", - ClientVersion = "1.0.0", - UpgradeClientVersion = "0.5.0", - InstallPath = _testDir, - AppSecretKey = "prod-key", - ProductId = "product-001", - Scheme = "Bearer", - Token = "jwt-token" - }; - - var updatePrecheckCalled = false; - var updateInfoReceived = false; - - var bootstrap = new GeneralUpdateBootstrap() - .SetConfig(config) - .AddListenerUpdatePrecheck(args => - { - updatePrecheckCalled = true; - return false; // Don't skip, proceed with update - }) - .AddListenerUpdateInfo((s, e) => - { - updateInfoReceived = true; - }) - .AddListenerMultiAllDownloadCompleted((s, e) => { }) - .AddListenerMultiDownloadCompleted((s, e) => { }) - .AddListenerMultiDownloadError((s, e) => { }) - .AddListenerMultiDownloadStatistics((s, e) => { }) - .AddListenerException((s, e) => { }); - - // Assert - all components configured correctly - Assert.NotNull(bootstrap); - Assert.False(updatePrecheckCalled); - Assert.False(updateInfoReceived); - Assert.NotNull(bootstrap); - } - - /// - /// Scenario: Only main app needs update, upgrade is current. - /// - [Fact] - public void MutualUpgrade_MainAppOnly_ConfiguresClientCorrectly() - { - // Arrange - var config = new Configinfo - { - UpdateUrl = "https://api.example.com/updates", - MainAppName = "MyApp.exe", - ClientVersion = "1.0.0", - InstallPath = _testDir, - AppSecretKey = "key", - Scheme = "https", - Token = "token" - }; - - var bootstrap = new GeneralUpdateBootstrap() - .SetConfig(config); - - // Assert - Assert.NotNull(bootstrap); - } - - /// - /// Scenario: Upgrade app itself needs update but main app is current. - /// - [Fact] - public void MutualUpgrade_UpgradeAppOnly_ConfiguresClientCorrectly() - { - // Arrange - upgrade app needs updating but main doesn't - var config = new Configinfo - { - UpdateUrl = "https://api.example.com/updates", - MainAppName = "MyApp.exe", - ClientVersion = "2.0.0", - UpgradeClientVersion = "1.0.0", - InstallPath = _testDir, - AppSecretKey = "key", - Scheme = "https", - Token = "token" - }; - - var bootstrap = new GeneralUpdateBootstrap() - .SetConfig(config); - - // Assert - Assert.NotNull(bootstrap); - } - - #endregion - - #region Precheck / Skip Scenarios - - /// - /// Scenario: Developer wants to show a UI dialog before updating. - /// The precheck callback receives update info and returns user's decision. - /// - [Fact] - public void Precheck_UserChoosesToSkip_ReturnsTrue() - { - // Arrange - var config = new Configinfo - { - UpdateUrl = "https://api.example.com", - MainAppName = "MyApp.exe", - ClientVersion = "1.0.0", - InstallPath = _testDir, - AppSecretKey = "key", - Scheme = "https", - Token = "token" - }; - - var bootstrap = new GeneralUpdateBootstrap() - .SetConfig(config) - .AddListenerUpdatePrecheck(args => - { - // Real app would show dialog here and return user choice - return true; // User chose to skip - }); - - // Assert — precheck registered correctly (callback invoked only during LaunchAsync) - Assert.NotNull(bootstrap); - } - - /// - /// Scenario: Developer wants to skip update when already on current version. - /// - [Fact] - public void Precheck_SkipWhenNoUpdate_ReturnsCorrectDecision() - { - // Arrange - var config = new Configinfo - { - UpdateUrl = "https://api.example.com", - MainAppName = "MyApp.exe", - ClientVersion = "1.0.0", - InstallPath = _testDir, - AppSecretKey = "key", - Scheme = "https", - Token = "token" - }; - - var skipCalled = false; - - // Developer setup: only skip if certain conditions met - var bootstrap = new GeneralUpdateBootstrap() - .SetConfig(config) - .AddListenerUpdatePrecheck(args => - { - skipCalled = true; - // Real logic: check version and decide - return args.Info?.Body == null || - args.Info.Body.Count == 0; - }); - - Assert.NotNull(bootstrap); - Assert.False(skipCalled); - } - - /// - /// Scenario: Developer wants to auto-approve updates during off-hours. - /// - [Fact] - public void Precheck_AutoApproveDuringOffHours_ConfiguresLogicCorrectly() - { - // Arrange - var config = new Configinfo - { - UpdateUrl = "https://api.example.com", - MainAppName = "MyApp.exe", - ClientVersion = "1.0.0", - InstallPath = _testDir, - AppSecretKey = "key", - Scheme = "https", - Token = "token" - }; - - // Developer logic: auto-approve between 2 AM and 6 AM - var bootstrap = new GeneralUpdateBootstrap() - .SetConfig(config) - .AddListenerUpdatePrecheck(args => - { - var hour = DateTime.Now.Hour; - if (hour >= 2 && hour < 6) - return false; // Auto-approve during off-hours - return true; // Otherwise ask user - }); - - Assert.NotNull(bootstrap); - } - - #endregion - - #region Custom Options Injection - - /// - /// Scenario: Developer injects custom pre-update checks. - /// - [Fact] - public void CustomOptions_MultipleChecks_AllRegistered() - { - // Arrange - var config = new Configinfo - { - UpdateUrl = "https://api.example.com", - MainAppName = "MyApp.exe", - ClientVersion = "1.0.0", - InstallPath = _testDir, - AppSecretKey = "key", - Scheme = "https", - Token = "token" - }; - - // Developer registers multiple custom checks - var customOptions = new List> - { - () => Directory.Exists(_testDir), // Check install directory exists - () => true, // Check disk space - () => true // Check network connectivity - }; - - var bootstrap = new GeneralUpdateBootstrap() - .SetConfig(config) - .AddCustomOption(customOptions); - - Assert.NotNull(bootstrap); - } - - /// - /// Scenario: Developer injects empty custom options (no-op). - /// - [Fact] - public void CustomOptions_ValidList_DoesNotThrow() - { - // Arrange - var config = new Configinfo - { - UpdateUrl = "https://api.example.com", - MainAppName = "MyApp.exe", - ClientVersion = "1.0.0", - InstallPath = _testDir, - AppSecretKey = "key", - Scheme = "https", - Token = "token" - }; - - // Act & Assert — valid non-empty list should not throw (API requires non-empty) - var bootstrap = new GeneralUpdateBootstrap() - .SetConfig(config) - .AddCustomOption(new List> { () => true }); - - Assert.NotNull(bootstrap); - } - - #endregion - - #region Event Listener Chain (Client-Side) - - /// - /// Scenario: Developer sets up all event listeners for comprehensive monitoring. - /// - [Fact] - public void EventListeners_FullChain_AllSevenEventsRegistered() - { - // Arrange - var config = new Configinfo - { - UpdateUrl = "https://api.example.com", - MainAppName = "MyApp.exe", - ClientVersion = "1.0.0", - InstallPath = _testDir, - AppSecretKey = "key", - Scheme = "https", - Token = "token" - }; - - var eventsRegistered = new List(); - - // Act - developer chains all event listeners - var bootstrap = new GeneralUpdateBootstrap() - .SetConfig(config) - .AddListenerUpdateInfo((s, e) => eventsRegistered.Add("UpdateInfo")) - .AddListenerMultiAllDownloadCompleted((s, e) => eventsRegistered.Add("AllDownloaded")) - .AddListenerMultiDownloadCompleted((s, e) => eventsRegistered.Add("DownloadCompleted")) - .AddListenerMultiDownloadError((s, e) => eventsRegistered.Add("DownloadError")) - .AddListenerMultiDownloadStatistics((s, e) => eventsRegistered.Add("Statistics")) - .AddListenerException((s, e) => eventsRegistered.Add("Exception")); - - // Assert - all listeners registered - Assert.NotNull(bootstrap); - } - - /// - /// Scenario: Developer only listens for critical events. - /// - [Fact] - public void EventListeners_CriticalOnly_ExceptionAndUpdateInfo() - { - // Arrange - var config = new Configinfo - { - UpdateUrl = "https://api.example.com", - MainAppName = "MyApp.exe", - ClientVersion = "1.0.0", - InstallPath = _testDir, - AppSecretKey = "key", - Scheme = "https", - Token = "token" - }; - - // Developer only cares about errors and update info - var bootstrap = new GeneralUpdateBootstrap() - .SetConfig(config) - .AddListenerException((s, e) => { /* Log error for telemetry */ }) - .AddListenerUpdateInfo((s, e) => { /* Show update available toast */ }); - - Assert.NotNull(bootstrap); - } - - #endregion - - #region Method Chaining (Fluent API) - - /// - /// Scenario: Developer uses fluent API to configure everything in one chain. - /// - [Fact] - public void FluentApi_FullChain_ReturnsCorrectBootstrapInstance() - { - // Arrange & Act - complete fluent chain - var config = new Configinfo - { - UpdateUrl = "https://api.example.com", - AppName = "Update.exe", - MainAppName = "MyApp.exe", - ClientVersion = "1.0.0", - UpgradeClientVersion = "1.0.0", - InstallPath = _testDir, - AppSecretKey = "secret", - ProductId = "app-001", - Scheme = "Bearer", - Token = "jwt", - Bowl = "Bowl.exe", - Script = "#!/bin/bash\nchmod +x", - ReportUrl = "https://telemetry.example.com", - UpdateLogUrl = "https://example.com/changelog", - BlackFiles = new List { "*.pdb" }, - BlackFormats = new List { ".log" }, - SkipDirectorys = new List { "logs" } - }; - - var bootstrap = new GeneralUpdateBootstrap() - .SetConfig(config) - .AddListenerUpdatePrecheck(args => false) - .AddCustomOption(new List> { () => true }) - .AddListenerUpdateInfo((s, e) => { }) - .AddListenerMultiAllDownloadCompleted((s, e) => { }) - .AddListenerMultiDownloadCompleted((s, e) => { }) - .AddListenerMultiDownloadError((s, e) => { }) - .AddListenerMultiDownloadStatistics((s, e) => { }) - .AddListenerException((s, e) => { }); - - // Assert - Assert.NotNull(bootstrap); - } - - /// - /// Scenario: Developer uses minimal fluent API - only essential configuration. - /// - [Fact] - public void FluentApi_MinimalChain_JustConfig() - { - // The minimum a developer MUST provide - var bootstrap = 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" - }); - - Assert.NotNull(bootstrap); - } - - #endregion - - #region Silent Update Configuration - - /// - /// Scenario: Developer configures silent update mode. - /// App checks for updates silently in the background. - /// - [Fact] - public void SilentUpdate_Configuration_IsValid() - { - // Arrange - developer sets up silent update - var config = new Configinfo - { - UpdateUrl = "https://api.example.com", - MainAppName = "MyApp.exe", - ClientVersion = "1.0.0", - InstallPath = _testDir, - AppSecretKey = "key", - Scheme = "https", - Token = "token" - }; - - // Note: SilentUpdateMode requires EnableSilentUpdate option to be set on AbstractBootstrap - var bootstrap = new GeneralUpdateBootstrap() - .SetConfig(config); - - Assert.NotNull(bootstrap); - } - - #endregion - - #region Configinfo Edge Cases - - /// - /// Tests Configinfo with null list properties doesn't cause issues. - /// - [Fact] - public void Configinfo_NullLists_SetDefaults() - { - var config = new Configinfo - { - UpdateUrl = "https://api.example.com", - MainAppName = "MyApp.exe", - ClientVersion = "1.0.0", - AppSecretKey = "key", - Scheme = "https", - Token = "token" - // BlackFiles, BlackFormats, SkipDirectorys left as default (null) - }; - - config.Validate(); - Assert.NotNull(config); - } - - /// - /// Tests Configinfo default InstallPath is set correctly. - /// - [Fact] - public void Configinfo_DefaultInstallPath_IsCurrentDirectory() - { - var config = new Configinfo - { - UpdateUrl = "https://api.example.com", - MainAppName = "MyApp.exe", - ClientVersion = "1.0.0", - AppSecretKey = "key", - Scheme = "https", - Token = "token" - }; - - // Default InstallPath should be the current base directory - Assert.Equal(AppDomain.CurrentDomain.BaseDirectory, config.InstallPath); - } - - /// - /// Tests Configinfo default AppName is "Update.exe". - /// - [Fact] - public void Configinfo_DefaultAppName_IsUpdateExe() - { - var config = new Configinfo - { - UpdateUrl = "https://api.example.com", - MainAppName = "MyApp.exe", - ClientVersion = "1.0.0", - AppSecretKey = "key", - Scheme = "https", - Token = "token" - }; - - Assert.Equal("Update.exe", config.AppName); - } - - #endregion - - #region Cross-Platform Considerations - - /// - /// Tests that Configinfo works correctly on any platform. - /// - [Fact] - public void Configinfo_PlatformAgnostic_WorksOnAnyOS() - { - var config = new Configinfo - { - UpdateUrl = "https://api.example.com", - MainAppName = "MyApp.exe", - ClientVersion = "1.0.0", - AppSecretKey = "key", - Scheme = "https", - Token = "token" - }; - - config.Validate(); - Assert.NotNull(config); - } - - /// - /// Tests that the Script field supports shell scripts for Linux/macOS. - /// - [Fact] - public void Configinfo_LinuxPermissionScript_StoredCorrectly() - { - var config = new Configinfo - { - UpdateUrl = "https://api.example.com", - MainAppName = "MyApp", - ClientVersion = "1.0.0", - AppSecretKey = "key", - Scheme = "https", - Token = "token", - Script = "#!/bin/bash\nset -e\nchmod +x /opt/app/Update\nchown root:root /opt/app/Update" - }; - - config.Validate(); - Assert.Contains("chmod +x", config.Script); - Assert.Contains("#!/bin/bash", config.Script); - } - - #endregion - - #region UpdateInfoEventArgs Tests - - /// - /// Tests UpdateInfoEventArgs creation with VersionRespDTO. - /// - [Fact] - public void UpdateInfoEventArgs_WithVersionResponse_ContainsCorrectData() - { - // Arrange - var versions = new List - { - new() { Version = "2.0.0", Url = "https://cdn.example.com/update.zip", Format = "ZIP", IsForcibly = true } - }; - - var response = new VersionRespDTO - { - Code = 200, - Body = versions - }; - - // Act - var args = new UpdateInfoEventArgs(response); - - // Assert - Assert.NotNull(args.Info); - Assert.Equal(200, args.Info.Code); - Assert.NotNull(args.Info.Body); - Assert.Single(args.Info.Body); - Assert.True(args.Info.Body[0].IsForcibly); - } - - /// - /// Tests UpdateInfoEventArgs with no-update response. - /// - [Fact] - public void UpdateInfoEventArgs_NoUpdateResponse_HasEmptyBody() - { - // Arrange - server says no update available - var response = new VersionRespDTO - { - Code = 200, - Body = new List() // empty - }; - - // Act - var args = new UpdateInfoEventArgs(response); - - // Assert - Assert.NotNull(args.Info); - Assert.Empty(args.Info.Body); - } - - /// - /// Tests UpdateInfoEventArgs with error response. - /// - [Fact] - public void UpdateInfoEventArgs_ErrorResponse_HasErrorCode() - { - // Arrange - server returns error - var response = new VersionRespDTO - { - Code = 500, - Body = null! - }; - - // Act - var args = new UpdateInfoEventArgs(response); - - // Assert - Assert.NotNull(args.Info); - Assert.Equal(500, args.Info.Code); - Assert.Null(args.Info.Body); - } - - #endregion - - #region ExceptionEventArgs Tests - - /// - /// Tests ExceptionEventArgs creation and properties. - /// - [Fact] - public void ExceptionEventArgs_WithException_ContainsExceptionData() - { - // Arrange - var ex = new InvalidOperationException("Update failed"); - - // Act - var args = new ExceptionEventArgs(ex, ex.Message); - - // Assert - Assert.NotNull(args.Exception); - Assert.Equal("Update failed", args.Exception.Message); - Assert.Equal("Update failed", args.Message); - } - - #endregion - } -} diff --git a/tests/ClientCoreTest/ClientCoreTest.csproj b/tests/ClientCoreTest/ClientCoreTest.csproj deleted file mode 100644 index f9238eb0..00000000 --- a/tests/ClientCoreTest/ClientCoreTest.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - net10.0 - enable - enable - false - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/tests/ClientCoreTest/Hubs/RandomRetryPolicyTests.cs b/tests/ClientCoreTest/Hubs/RandomRetryPolicyTests.cs deleted file mode 100644 index 0af9f1df..00000000 --- a/tests/ClientCoreTest/Hubs/RandomRetryPolicyTests.cs +++ /dev/null @@ -1,175 +0,0 @@ -using System; -using GeneralUpdate.Core.Hubs; -using Microsoft.AspNetCore.SignalR.Client; -using Xunit; - -namespace ClientCoreTest.Hubs -{ - /// - /// Contains test cases for the RandomRetryPolicy class. - /// Tests retry logic for SignalR connection failures. - /// - public class RandomRetryPolicyTests - { - /// - /// Tests that NextRetryDelay returns a value when elapsed time is less than 60 seconds. - /// - [Fact] - public void NextRetryDelay_WithLessThan60Seconds_ReturnsDelay() - { - // Arrange - var policy = new RandomRetryPolicy(); - var context = new RetryContext - { - PreviousRetryCount = 1, - ElapsedTime = TimeSpan.FromSeconds(30), - RetryReason = new Exception("Test exception") - }; - - // Act - var delay = policy.NextRetryDelay(context); - - // Assert - Assert.NotNull(delay); - Assert.True(delay.Value.TotalSeconds >= 0); - Assert.True(delay.Value.TotalSeconds <= 10); - } - - /// - /// Tests that NextRetryDelay returns null when elapsed time is 60 seconds or more. - /// - [Fact] - public void NextRetryDelay_WithMoreThan60Seconds_ReturnsNull() - { - // Arrange - var policy = new RandomRetryPolicy(); - var context = new RetryContext - { - PreviousRetryCount = 10, - ElapsedTime = TimeSpan.FromSeconds(60), - RetryReason = new Exception("Test exception") - }; - - // Act - var delay = policy.NextRetryDelay(context); - - // Assert - Assert.Null(delay); - } - - /// - /// Tests that NextRetryDelay returns null when elapsed time exceeds 60 seconds. - /// - [Fact] - public void NextRetryDelay_WithGreaterThan60Seconds_ReturnsNull() - { - // Arrange - var policy = new RandomRetryPolicy(); - var context = new RetryContext - { - PreviousRetryCount = 15, - ElapsedTime = TimeSpan.FromSeconds(120), - RetryReason = new Exception("Test exception") - }; - - // Act - var delay = policy.NextRetryDelay(context); - - // Assert - Assert.Null(delay); - } - - /// - /// Tests that NextRetryDelay returns a delay for first retry attempt. - /// - [Fact] - public void NextRetryDelay_OnFirstRetry_ReturnsDelay() - { - // Arrange - var policy = new RandomRetryPolicy(); - var context = new RetryContext - { - PreviousRetryCount = 0, - ElapsedTime = TimeSpan.FromSeconds(5), - RetryReason = new Exception("Test exception") - }; - - // Act - var delay = policy.NextRetryDelay(context); - - // Assert - Assert.NotNull(delay); - } - - /// - /// Tests that NextRetryDelay boundary condition at exactly 60 seconds. - /// - [Fact] - public void NextRetryDelay_AtExactly60Seconds_ReturnsNull() - { - // Arrange - var policy = new RandomRetryPolicy(); - var context = new RetryContext - { - PreviousRetryCount = 10, - ElapsedTime = TimeSpan.FromSeconds(60), - RetryReason = new Exception("Test exception") - }; - - // Act - var delay = policy.NextRetryDelay(context); - - // Assert - Assert.Null(delay); - } - - /// - /// Tests that NextRetryDelay boundary condition just before 60 seconds. - /// - [Fact] - public void NextRetryDelay_JustBefore60Seconds_ReturnsDelay() - { - // Arrange - var policy = new RandomRetryPolicy(); - var context = new RetryContext - { - PreviousRetryCount = 8, - ElapsedTime = TimeSpan.FromSeconds(59.99), - RetryReason = new Exception("Test exception") - }; - - // Act - var delay = policy.NextRetryDelay(context); - - // Assert - Assert.NotNull(delay); - } - - /// - /// Tests that multiple calls to NextRetryDelay produce random values. - /// - [Fact] - public void NextRetryDelay_MultipleCalls_ProducesVariation() - { - // Arrange - var policy = new RandomRetryPolicy(); - var context = new RetryContext - { - PreviousRetryCount = 1, - ElapsedTime = TimeSpan.FromSeconds(10), - RetryReason = new Exception("Test exception") - }; - - // Act - Get multiple delays - var delays = new TimeSpan?[5]; - for (int i = 0; i < 5; i++) - { - delays[i] = policy.NextRetryDelay(context); - } - - // Assert - At least some variation in delays (not all exactly the same) - Assert.All(delays, d => Assert.NotNull(d)); - Assert.All(delays, d => Assert.True(d!.Value.TotalSeconds >= 0 && d.Value.TotalSeconds <= 10)); - } - } -} diff --git a/tests/ClientCoreTest/Hubs/UpgradeHubServiceTests.cs b/tests/ClientCoreTest/Hubs/UpgradeHubServiceTests.cs deleted file mode 100644 index cc057804..00000000 --- a/tests/ClientCoreTest/Hubs/UpgradeHubServiceTests.cs +++ /dev/null @@ -1,249 +0,0 @@ -using System; -using System.Threading.Tasks; -using GeneralUpdate.Core.Hubs; -using Xunit; - -namespace ClientCoreTest.Hubs -{ - /// - /// Contains test cases for the UpgradeHubService class. - /// Tests SignalR hub connection and event listener management. - /// - public class UpgradeHubServiceTests - { - /// - /// Tests that constructor creates service with valid URL. - /// - [Fact] - public void Constructor_WithValidUrl_CreatesService() - { - // Arrange - var url = "http://localhost:5000/upgradeHub"; - - // Act - var service = new UpgradeHubService(url); - - // Assert - Assert.NotNull(service); - } - - /// - /// Tests that constructor creates service with URL and token. - /// - [Fact] - public void Constructor_WithUrlAndToken_CreatesService() - { - // Arrange - var url = "http://localhost:5000/upgradeHub"; - var token = "test-token-12345"; - - // Act - var service = new UpgradeHubService(url, token); - - // Assert - Assert.NotNull(service); - } - - /// - /// Tests that constructor creates service with URL, token, and appkey. - /// - [Fact] - public void Constructor_WithUrlTokenAndAppKey_CreatesService() - { - // Arrange - var url = "http://localhost:5000/upgradeHub"; - var token = "test-token-12345"; - var appkey = "test-appkey"; - - // Act - var service = new UpgradeHubService(url, token, appkey); - - // Assert - Assert.NotNull(service); - } - - /// - /// Tests that AddListenerReceive can register a callback. - /// - [Fact] - public void AddListenerReceive_WithCallback_RegistersListener() - { - // Arrange - var url = "http://localhost:5000/upgradeHub"; - var service = new UpgradeHubService(url); - var callbackInvoked = false; - Action callback = (message) => { callbackInvoked = true; }; - - // Act - service.AddListenerReceive(callback); - - // Assert - Callback was registered (no exception thrown) - Assert.False(callbackInvoked); // Not invoked yet - } - - /// - /// Tests that AddListenerOnline can register a callback. - /// - [Fact] - public void AddListenerOnline_WithCallback_RegistersListener() - { - // Arrange - var url = "http://localhost:5000/upgradeHub"; - var service = new UpgradeHubService(url); - var callbackInvoked = false; - Action callback = (message) => { callbackInvoked = true; }; - - // Act - service.AddListenerOnline(callback); - - // Assert - Callback was registered (no exception thrown) - Assert.False(callbackInvoked); // Not invoked yet - } - - /// - /// Tests that AddListenerReconnected can register a callback. - /// - [Fact] - public void AddListenerReconnected_WithCallback_RegistersListener() - { - // Arrange - var url = "http://localhost:5000/upgradeHub"; - var service = new UpgradeHubService(url); - Func callback = async (connectionId) => { await Task.CompletedTask; }; - - // Act - service.AddListenerReconnected(callback); - - // Assert - Callback was registered (no exception thrown) - Assert.True(true); - } - - /// - /// Tests that AddListenerClosed can register a callback. - /// - [Fact] - public void AddListenerClosed_WithCallback_RegistersListener() - { - // Arrange - var url = "http://localhost:5000/upgradeHub"; - var service = new UpgradeHubService(url); - Func callback = async (exception) => { await Task.CompletedTask; }; - - // Act - service.AddListenerClosed(callback); - - // Assert - Callback was registered (no exception thrown) - Assert.True(true); - } - - /// - /// Tests that multiple listeners can be registered. - /// - [Fact] - public void MultipleListeners_CanBeRegistered() - { - // Arrange - var url = "http://localhost:5000/upgradeHub"; - var service = new UpgradeHubService(url); - - Action receiveCallback = (message) => { }; - Action onlineCallback = (message) => { }; - Func reconnectedCallback = async (connectionId) => { await Task.CompletedTask; }; - Func closedCallback = async (exception) => { await Task.CompletedTask; }; - - // Act - service.AddListenerReceive(receiveCallback); - service.AddListenerOnline(onlineCallback); - service.AddListenerReconnected(reconnectedCallback); - service.AddListenerClosed(closedCallback); - - // Assert - All callbacks were registered (no exception thrown) - Assert.True(true); - } - - /// - /// Tests that StartAsync can be called (will fail to connect without server). - /// - [Fact] - public async Task StartAsync_WithoutServer_HandlesGracefully() - { - // Arrange - var url = "http://localhost:9999/upgradeHub"; // Non-existent server - var service = new UpgradeHubService(url); - - // Act & Assert - Should handle connection failure gracefully - await service.StartAsync(); // Logs error but doesn't throw - Assert.True(true); - } - - /// - /// Tests that StopAsync can be called. - /// - [Fact] - public async Task StopAsync_CanBeCalled() - { - // Arrange - var url = "http://localhost:5000/upgradeHub"; - var service = new UpgradeHubService(url); - - // Act - await service.StopAsync(); - - // Assert - No exception thrown - Assert.True(true); - } - - /// - /// Tests that DisposeAsync can be called. - /// - [Fact] - public async Task DisposeAsync_CanBeCalled() - { - // Arrange - var url = "http://localhost:5000/upgradeHub"; - var service = new UpgradeHubService(url); - - // Act - await service.DisposeAsync(); - - // Assert - No exception thrown - Assert.True(true); - } - - /// - /// Tests that service lifecycle methods can be called in sequence. - /// - [Fact] - public async Task ServiceLifecycle_CanBeExecutedInSequence() - { - // Arrange - var url = "http://localhost:9999/upgradeHub"; - var service = new UpgradeHubService(url); - - // Act - await service.StartAsync(); - await service.StopAsync(); - await service.DisposeAsync(); - - // Assert - No exception thrown - Assert.True(true); - } - - /// - /// Tests that IUpgradeHubService interface is properly implemented. - /// - [Fact] - public void UpgradeHubService_ImplementsInterface() - { - // Arrange - var url = "http://localhost:5000/upgradeHub"; - - // Act - IUpgradeHubService service = new UpgradeHubService(url); - - // Assert - Assert.NotNull(service); - Assert.IsAssignableFrom(service); - } - } -} diff --git a/tests/ClientCoreTest/README.md b/tests/ClientCoreTest/README.md deleted file mode 100644 index d081dc88..00000000 --- a/tests/ClientCoreTest/README.md +++ /dev/null @@ -1,195 +0,0 @@ -# ClientCoreTest - Unit Tests for GeneralUpdate.ClientCore - -## Overview - -This test project provides comprehensive unit test coverage for the GeneralUpdate.ClientCore component. The tests validate the functionality of the client-side update system, including configuration, strategies, pipelines, and hub services. - -## Test Structure - -The test project is organized into the following categories: - -### Bootstrap Tests (`Bootstrap/`) -- **GeneralClientBootstrapTests.cs** - Tests for the main bootstrap class - - Configuration methods (SetConfig, SetCustomSkipOption, AddCustomOption) - - Event listener registrations - - Fluent interface pattern - - Validation logic - - Method chaining - -### OSS Tests (`OSS/`) -- **GeneralClientOSSTests.cs** - Tests for OSS (Object Storage Service) update functionality - - Version comparison logic - - Configuration serialization - - Start method workflow - -### Strategy Tests (`Strategy/`) -- **WindowsStrategyTests.cs** - Tests for Windows platform update strategy - - Strategy initialization - - Pipeline creation - - Configuration handling -- **LinuxStrategyTests.cs** - Tests for Linux platform update strategy - - Strategy initialization with blacklist support - - Pipeline creation - - Blacklist file/format handling - -### Pipeline Tests (`Pipeline/`) -- **HashMiddlewareTests.cs** - Tests for hash verification middleware - - SHA256 hash verification - - Case-insensitive comparison - - Error handling for invalid/missing hashes -- **CompressMiddlewareTests.cs** - Tests for compression middleware - - Context parameter handling - - Format/encoding validation -- **PatchMiddlewareTests.cs** - Tests for differential patch middleware - - Source and target path handling - - DifferentialCore integration - -### Hub Tests (`Hubs/`) -- **UpgradeHubServiceTests.cs** - Tests for SignalR hub service - - Connection lifecycle (Start, Stop, Dispose) - - Event listener registration - - Multiple listener support - - Interface implementation -- **RandomRetryPolicyTests.cs** - Tests for retry policy - - Retry timing logic (< 60 seconds) - - Retry termination (>= 60 seconds) - - Random delay generation - -## Test Statistics - -- **Total Tests**: 88 -- **Passing**: 88 -- **Failing**: 0 -- **Test Framework**: xUnit 2.9.3 -- **Mocking Framework**: Moq 4.20.72 - -## Test Categories - -### Component Distribution -- Bootstrap: 16 tests -- OSS: 10 tests -- Strategy: 14 tests (7 Windows + 7 Linux) -- Pipeline: 28 tests (9 Hash + 11 Compress + 8 Patch) -- Hubs: 20 tests (13 UpgradeHubService + 7 RandomRetryPolicy) - -### Test Types -- Unit Tests: 88 -- Integration Tests: 0 -- End-to-End Tests: 0 - -## Running the Tests - -### Run all tests -```bash -dotnet test src/c#/ClientCoreTest/ClientCoreTest.csproj -``` - -### Run tests with detailed output -```bash -dotnet test src/c#/ClientCoreTest/ClientCoreTest.csproj --verbosity detailed -``` - -### Run specific test class -```bash -dotnet test src/c#/ClientCoreTest/ClientCoreTest.csproj --filter "FullyQualifiedName~GeneralClientBootstrapTests" -``` - -### Run tests with coverage -```bash -dotnet test src/c#/ClientCoreTest/ClientCoreTest.csproj /p:CollectCoverage=true -``` - -## Key Testing Patterns - -### 1. Fluent Interface Testing -Tests verify that methods return the bootstrap instance for method chaining: -```csharp -var result = bootstrap - .SetConfig(config) - .SetCustomSkipOption(() => false) - .AddListenerException((s, e) => { }); -Assert.Same(bootstrap, result); -``` - -### 2. Event Listener Testing -Tests verify that event listeners can be registered without throwing exceptions: -```csharp -Action callback = (sender, args) => { }; -var result = bootstrap.AddListenerException(callback); -Assert.NotNull(result); -``` - -### 3. Async Middleware Testing -Tests verify asynchronous pipeline middleware behavior: -```csharp -var middleware = new HashMiddleware(); -await middleware.InvokeAsync(context); -``` - -### 4. Strategy Factory Testing -Tests verify platform-specific strategy creation: -```csharp -var strategy = new WindowsStrategy(); -strategy.Create(config); -Assert.True(true); // No exception means success -``` - -## Dependencies - -- **.NET 10.0** - Target framework -- **xUnit 2.9.3** - Testing framework -- **Moq 4.20.72** - Mocking framework -- **Microsoft.NET.Test.Sdk 17.14.1** - Test SDK -- **coverlet.collector 6.0.4** - Code coverage collection - -## Test Coverage - -The tests cover the following components: -- ✅ GeneralClientBootstrap - Configuration and lifecycle -- ✅ GeneralClientOSS - OSS update functionality -- ✅ WindowsStrategy - Windows platform strategy -- ✅ LinuxStrategy - Linux platform strategy -- ✅ HashMiddleware - Hash verification -- ✅ CompressMiddleware - Decompression -- ✅ PatchMiddleware - Differential patching -- ✅ UpgradeHubService - SignalR hub integration -- ✅ RandomRetryPolicy - Retry logic - -## Notes - -### Assertion Testing -Some tests handle Debug.Assert behavior which differs between debug and release builds: -- In debug mode: Assertions throw exceptions -- In release mode: Assertions may be optimized out -- Tests are designed to handle both scenarios - -### Private Method Testing -Some private methods are tested indirectly through public API: -- Version comparison logic in GeneralClientOSS -- Pipeline context creation in strategies -- This maintains encapsulation while ensuring functionality - -### Lifecycle Testing -Hub service lifecycle tests verify graceful handling when no server is available: -- StartAsync handles connection failures gracefully -- StopAsync and DisposeAsync don't throw exceptions -- Useful for testing resilience - -## Future Enhancements - -Potential areas for additional testing: -- Integration tests with actual SignalR server -- End-to-end update workflow tests -- Performance/stress testing for large updates -- Concurrent update scenario testing -- Network failure simulation tests - -## Contributing - -When adding new tests: -1. Follow existing naming conventions -2. Include XML documentation comments -3. Group related tests in the same file -4. Use descriptive test method names -5. Add tests for both success and failure paths -6. Ensure tests are independent and can run in any order diff --git a/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs b/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs index b3647c39..7b4c59a3 100644 --- a/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs +++ b/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Text; using GeneralUpdate.Core; @@ -10,10 +9,9 @@ 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. + /// Full parameter matrix tests -- verifies ALL framework-level UpdateOptions + /// can be set via .Option() without throwing. Business fields (UpdateUrl, Token, etc.) + /// are now stored in Configinfo, not in UpdateOptions. /// public class BootstrapFullParameterMatrixTests : IDisposable { @@ -63,18 +61,6 @@ public void Dispose() [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)] @@ -91,20 +77,6 @@ public void Dispose() [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)); @@ -115,14 +87,12 @@ public void Dispose() [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() + [Fact] public void Chain_AllFrameworkOptions() { var b = new GeneralUpdateBootstrap() .Option(UpdateOptions.AppType, AppType.Client) @@ -135,13 +105,6 @@ [Fact] public void Chain_All33Options() .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) @@ -149,15 +112,7 @@ [Fact] public void Chain_All33Options() .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", @@ -199,59 +154,25 @@ [Fact] public void Chain_UpgradeNoBackup() 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() + [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", + 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" } + Scheme = "HMAC", Token = "hmac-prod-secret", Bowl = "Bowl.exe", + Script = "#!/bin/bash\nchmod +x /opt/enterprise/Update", + BlackFiles = new List { "*.pdb" }, + BlackFormats = new List { ".log", ".tmp" }, + SkipDirectorys = new List { "logs", "temp" } }; - var clientBootstrap = new GeneralUpdateBootstrap() + var client = new GeneralUpdateBootstrap() .Option(UpdateOptions.AppType, AppType.Client) .Option(UpdateOptions.DiffMode, DiffMode.Parallel) .Option(UpdateOptions.Encoding, Encoding.UTF8) @@ -262,31 +183,13 @@ public void Chain_ClientAndUpgrade_BothFullyConfigured() .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") + new List { "*.pdb" }, new List { ".log" }, new List { "logs" })) .SetConfig(sharedConfig) .AddListenerUpdatePrecheck(args => { @@ -298,15 +201,10 @@ public void Chain_ClientAndUpgrade_BothFullyConfigured() .AddListenerMultiDownloadCompleted((s, e) => { }) .AddListenerMultiDownloadError((s, e) => { }) .AddListenerMultiDownloadStatistics((s, e) => { }) - .AddListenerException((s, e) => { }) - .AddCustomOption(new List> - { - () => true, () => true, () => true - }); - - Assert.NotNull(clientBootstrap); + .AddListenerException((s, e) => { }); + Assert.NotNull(client); - var upgradeBootstrap = new GeneralUpdateBootstrap() + var upgrade = new GeneralUpdateBootstrap() .Option(UpdateOptions.AppType, AppType.Upgrade) .Option(UpdateOptions.DiffMode, DiffMode.Parallel) .Option(UpdateOptions.Encoding, Encoding.UTF8) @@ -316,124 +214,13 @@ public void Chain_ClientAndUpgrade_BothFullyConfigured() .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); } diff --git a/tests/CoreTest/Bootstrap/BootstrapHooksAndExtensionsTests.cs b/tests/CoreTest/Bootstrap/BootstrapHooksAndExtensionsTests.cs index 612416cd..9f447218 100644 --- a/tests/CoreTest/Bootstrap/BootstrapHooksAndExtensionsTests.cs +++ b/tests/CoreTest/Bootstrap/BootstrapHooksAndExtensionsTests.cs @@ -16,16 +16,6 @@ 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 { @@ -51,86 +41,39 @@ private sealed class TrackingHooks : IUpdateHooks 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; - } + 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 void TrackingHooks_InitialState_AllFlagsFalse() { var h = new TrackingHooks(); Assert.False(h.BeforeUpdateCalled); Assert.False(h.ErrorCalled); } [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); + Assert.Equal("1.0.0", hooks.BeforeCtx!.CurrentVersion); } [Fact] public async Task TrackingHooks_OnBeforeUpdate_CanReject() { - var rejectingHooks = new RejectingHooks(); + var hooks = 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"); + Assert.False(await hooks.OnBeforeUpdateAsync(ctx)); } private sealed class RejectingHooks : IUpdateHooks @@ -147,14 +90,8 @@ 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); + Assert.True(hooks.DownloadCtx!.Success); } [Fact] @@ -162,22 +99,15 @@ 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); + 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); - + await hooks.OnAfterUpdateAsync(new UpdateContext("App.exe", "/app", "1.0.0", "2.0.0", AppType.Client)); Assert.True(hooks.AfterUpdateCalled); } @@ -185,45 +115,23 @@ public async Task TrackingHooks_OnAfterUpdate_RecordsCall() 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); + await hooks.OnUpdateErrorAsync(new UpdateContext("App.exe", "/app", "1.0.0", "2.0.0", AppType.Client), ex); + Assert.Equal("Hash verification failed", hooks.CapturedError!.Message); } [Fact] - public async Task TrackingHooks_OnBeforeStartApp_RecordsCall() + public async Task TrackingHooks_FullLifecycle_AllMethodsCalled() { 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 + var uctx = new UpdateContext("App.exe", "/app", "1.0.0", "2.0.0", AppType.Client); + var dctx = new DownloadContext("pkg.zip", "2.0.0", 100, TimeSpan.FromSeconds(10), "/tmp/pkg.zip", true); + await hooks.OnBeforeUpdateAsync(uctx); + await hooks.OnDownloadCompletedAsync(dctx); + await hooks.OnAfterUpdateAsync(uctx); + await hooks.OnBeforeStartAppAsync(uctx); + Assert.True(hooks.BeforeUpdateCalled && hooks.DownloadCompletedCalled && hooks.AfterUpdateCalled && hooks.BeforeStartAppCalled); + Assert.False(hooks.ErrorCalled); } #endregion @@ -234,70 +142,11 @@ public async Task TrackingHooks_FullLifecycle_AllFiveMethodsCalled() 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); + Assert.True(await hooks.OnBeforeUpdateAsync(new UpdateContext("App.exe", "/app", "1.0.0", "2.0.0", AppType.Client))); + await hooks.OnDownloadCompletedAsync(new DownloadContext("pkg.zip", "2.0.0", 100, TimeSpan.FromSeconds(10), "/tmp/pkg.zip", true)); + await hooks.OnAfterUpdateAsync(new UpdateContext("App.exe", "/app", "1.0.0", "2.0.0", AppType.Client)); + await hooks.OnUpdateErrorAsync(new UpdateContext("App.exe", "/app", "1.0.0", null, AppType.Client), new Exception("test")); + await hooks.OnBeforeStartAppAsync(new UpdateContext("App.exe", "/app", "2.0.0", null, AppType.Client)); } #endregion @@ -307,43 +156,20 @@ public void DownloadContext_Failed() [Fact] public void UpdateReport_StartedEvent() { - var report = new UpdateReport("MyApp.exe", "1.0.0", "2.0.0", - UpdateEvent.UpdateStarted, AppType.Client, - DateTimeOffset.UtcNow); - + 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); + var report = new UpdateReport("App.exe", "1.0.0", "2.0.0", UpdateEvent.UpdateFailed, AppType.Client, + DateTimeOffset.UtcNow, ErrorMessage: "Disk full", DurationMs: 15000.0); + Assert.Equal("Disk full", 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() { @@ -363,35 +189,20 @@ public void UpdateEvent_AllValues_AreDefined() 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" }; - + var vi = 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.OnDownloadCompleted(new MultiDownloadCompletedEventArgs(vi, true)); + listener.OnDownloadError(new MultiDownloadErrorEventArgs(new Exception("test"), vi)); + listener.OnDownloadStatistics(new MultiDownloadStatisticsEventArgs(vi, 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); - } + Assert.True(listener.AllDownloadCalled && listener.DownloadCompletedCalled && listener.UpdateInfoCalled && listener.ExceptionCalled && 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 bool AllDownloadCalled, DownloadCompletedCalled, DownloadErrorCalled, StatisticsCalled, UpdateInfoCalled, ExceptionCalled, ProgressCalled; public void OnAllDownloadCompleted(MultiAllDownloadCompletedEventArgs e) => AllDownloadCalled = true; public void OnDownloadCompleted(MultiDownloadCompletedEventArgs e) => DownloadCompletedCalled = true; public void OnDownloadError(MultiDownloadErrorEventArgs e) => DownloadErrorCalled = true; @@ -399,86 +210,35 @@ private sealed class TestEventListener : IUpdateEventListener 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 + #region UpdateContext & DownloadContext [Fact] - public void AuthSchemeAndToken_CanBeConfigured() + public void UpdateContext_AllFields_SetCorrectly() { - 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); + var ctx = new UpdateContext("MyApp.exe", "/opt/app", "3.2.1", "4.0.0", AppType.Client); + Assert.Equal("MyApp.exe", ctx.AppName); + Assert.Equal("3.2.1", ctx.CurrentVersion); + Assert.Equal("4.0.0", ctx.TargetVersion); } [Fact] - public void AllAuthSchemes_CanBeConfigured() + public void DownloadContext_Successful() { - 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); - } + var ctx = new DownloadContext("client-v2.0.0.zip", "2.0.0", 50L * 1024 * 1024, TimeSpan.FromMinutes(2), "/tmp/update.zip", true); + Assert.Equal("client-v2.0.0.zip", ctx.AssetName); + Assert.True(ctx.Success); } - #endregion - - #region Full Extension Chain - [Fact] - public void Bootstrap_FullExtensions_AllConfigured() + public void DownloadContext_Failed() { - 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); + var ctx = new DownloadContext("failed.zip", "2.0.0", 0, TimeSpan.FromSeconds(3), null, false); + Assert.False(ctx.Success); + Assert.Null(ctx.LocalPath); } #endregion