diff --git a/src/c#/DrivelutionTest/Core/DriverUpdaterFactoryTests.cs b/src/c#/DrivelutionTest/Core/DriverUpdaterFactoryTests.cs index 0ae1c236..831ba72c 100644 --- a/src/c#/DrivelutionTest/Core/DriverUpdaterFactoryTests.cs +++ b/src/c#/DrivelutionTest/Core/DriverUpdaterFactoryTests.cs @@ -77,17 +77,11 @@ public void IsPlatformSupported_ReturnsBooleanValue() [Fact] public void CreateValidator_WithoutParameters_ReturnsNonNullInstance() { - // Skip on MacOS and Unknown platforms var platform = DrivelutionFactory.GetCurrentPlatform(); - if (platform == "MacOS" || platform == "Unknown") - { + if (platform == "Unknown") return; - } - // Act var validator = DrivelutionFactory.CreateValidator(); - - // Assert Assert.NotNull(validator); Assert.IsAssignableFrom(validator); } @@ -98,17 +92,11 @@ public void CreateValidator_WithoutParameters_ReturnsNonNullInstance() [Fact] public void CreateBackup_WithoutParameters_ReturnsNonNullInstance() { - // Skip on MacOS and Unknown platforms var platform = DrivelutionFactory.GetCurrentPlatform(); - if (platform == "MacOS" || platform == "Unknown") - { + if (platform == "Unknown") return; - } - // Act var backup = DrivelutionFactory.CreateBackup(); - - // Assert Assert.NotNull(backup); Assert.IsAssignableFrom(backup); } @@ -120,17 +108,15 @@ public void CreateBackup_WithoutParameters_ReturnsNonNullInstance() [Fact] public void Create_OnSupportedPlatform_DoesNotThrow() { - // Skip on MacOS as it's not yet implemented var platform = DrivelutionFactory.GetCurrentPlatform(); - - if (platform == "MacOS") + + if (platform == "Unknown") { - // MacOS should throw PlatformNotSupportedException Assert.Throws(() => DrivelutionFactory.Create()); return; } - // Act & Assert - should not throw on Windows/Linux + // Windows, Linux, and MacOS should all work var exception = Record.Exception(() => DrivelutionFactory.Create()); Assert.Null(exception); } diff --git a/src/c#/DrivelutionTest/Execution/CommandResultTests.cs b/src/c#/DrivelutionTest/Execution/CommandResultTests.cs new file mode 100644 index 00000000..c23b0ef3 --- /dev/null +++ b/src/c#/DrivelutionTest/Execution/CommandResultTests.cs @@ -0,0 +1,28 @@ +using GeneralUpdate.Drivelution.Core.Execution; + +namespace DrivelutionTest.Execution; + +public class CommandResultTests +{ + [Fact] + public void Success_WhenExitCodeZero_ReturnsTrue() + { + var result = new CommandResult { ExitCode = 0 }; + Assert.True(result.Success); + } + + [Fact] + public void Success_WhenExitCodeNonZero_ReturnsFalse() + { + var result = new CommandResult { ExitCode = 1 }; + Assert.False(result.Success); + } + + [Fact] + public void ToString_IncludesExitCode() + { + var result = new CommandResult { ExitCode = 0, StandardOutput = "hello" }; + Assert.Contains("0", result.ToString()); + Assert.Contains("hello", result.ToString()); + } +} diff --git a/src/c#/DrivelutionTest/GeneralDrivelutionTests.cs b/src/c#/DrivelutionTest/GeneralDrivelutionTests.cs index f9a26f7d..5417db24 100644 --- a/src/c#/DrivelutionTest/GeneralDrivelutionTests.cs +++ b/src/c#/DrivelutionTest/GeneralDrivelutionTests.cs @@ -250,7 +250,7 @@ public async Task QuickUpdateAsync_WithCancellationToken_CanBeCancelled() // Act & Assert // Should either complete quickly or throw cancellation exception var exception = await Record.ExceptionAsync( - () => GeneralDrivelution.QuickUpdateAsync(driverInfo, cts.Token)); + () => GeneralDrivelution.QuickUpdateAsync(driverInfo, cancellationToken: cts.Token)); // Either succeeded, failed gracefully, or was cancelled Assert.True(exception == null || exception is OperationCanceledException); @@ -274,14 +274,10 @@ public void GetPlatformInfo_ReportsCorrectSupportStatus() var platformInfo = GeneralDrivelution.GetPlatformInfo(); // Assert - if (platformInfo.Platform == "Windows" || platformInfo.Platform == "Linux") + if (platformInfo.Platform == "Windows" || platformInfo.Platform == "Linux" || platformInfo.Platform == "MacOS") { Assert.True(platformInfo.IsSupported); } - else if (platformInfo.Platform == "MacOS") - { - Assert.False(platformInfo.IsSupported); // MacOS not yet implemented - } } /// diff --git a/src/c#/DrivelutionTest/Models/NewModelTests.cs b/src/c#/DrivelutionTest/Models/NewModelTests.cs new file mode 100644 index 00000000..f16157c6 --- /dev/null +++ b/src/c#/DrivelutionTest/Models/NewModelTests.cs @@ -0,0 +1,80 @@ +using GeneralUpdate.Drivelution.Abstractions.Models; + +namespace DrivelutionTest.Models; + +/// +/// Tests for the new model classes added during the refactoring. +/// +public class NewModelTests +{ + [Fact] + public void UpdateProgress_HasExpectedDefaults() + { + var progress = new UpdateProgress(); + Assert.Equal(UpdateStatus.NotStarted, progress.CurrentStatus); + Assert.Equal(string.Empty, progress.StepName); + Assert.Equal(0, progress.Percentage); + Assert.Equal(0, progress.StepIndex); + Assert.Equal(0, progress.TotalSteps); + } + + [Fact] + public void UpdateProgress_ToString_IncludesAllFields() + { + var progress = new UpdateProgress + { + Percentage = 50, + StepName = "Validate", + StepIndex = 1, + TotalSteps = 4, + Message = "Checking" + }; + + var str = progress.ToString(); + Assert.Contains("50", str); + Assert.Contains("Validate", str); + Assert.Contains("2/4", str); + Assert.Contains("Checking", str); + } + + [Fact] + public void BatchUpdateResult_AllSucceeded_WhenAllPass() + { + var result = new BatchUpdateResult + { + SucceededCount = 3, + FailedCount = 0 + }; + result.AllSucceeded = result.FailedCount == 0; + + Assert.True(result.AllSucceeded); + } + + [Fact] + public void BatchUpdateResult_AllSucceeded_WhenAnyFail() + { + var result = new BatchUpdateResult + { + SucceededCount = 2, + FailedCount = 1 + }; + result.AllSucceeded = result.FailedCount == 0; + + Assert.False(result.AllSucceeded); + } + + [Fact] + public void DriverUpdateEntry_HoldsResult() + { + var updateResult = new UpdateResult { Success = true }; + var entry = new DriverUpdateEntry + { + DriverInfo = new DriverInfo { Name = "test" }, + Success = true, + Result = updateResult + }; + + Assert.Same(updateResult, entry.Result); + Assert.True(entry.Success); + } +} diff --git a/src/c#/DrivelutionTest/Pipeline/BaseDriverUpdaterTests.cs b/src/c#/DrivelutionTest/Pipeline/BaseDriverUpdaterTests.cs new file mode 100644 index 00000000..f72b87b9 --- /dev/null +++ b/src/c#/DrivelutionTest/Pipeline/BaseDriverUpdaterTests.cs @@ -0,0 +1,273 @@ +using GeneralUpdate.Drivelution.Abstractions; +using GeneralUpdate.Drivelution.Abstractions.Configuration; +using GeneralUpdate.Drivelution.Abstractions.Models; +using GeneralUpdate.Drivelution.Core.Pipeline; +using Moq; + +namespace DrivelutionTest.Pipeline; + +/// +/// Integration tests for BaseDriverUpdater using a minimal concrete subclass. +/// +public class BaseDriverUpdaterTests +{ + private readonly Mock _validatorMock; + private readonly Mock _backupMock; + private readonly DrivelutionOptions _options; + + public BaseDriverUpdaterTests() + { + _validatorMock = new Mock(); + _backupMock = new Mock(); + _options = new DrivelutionOptions { DefaultTimeoutSeconds = 10 }; + + // Default: all validations pass + _validatorMock.Setup(v => v.ValidateIntegrityAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + _validatorMock.Setup(v => v.ValidateSignatureAsync(It.IsAny(), It.IsAny>(), It.IsAny())) + .ReturnsAsync(true); + _validatorMock.Setup(v => v.ValidateCompatibilityAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + _backupMock.Setup(b => b.BackupAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + } + + [Fact] + public async Task UpdateAsync_WithValidInputs_Succeeds() + { + using var tempFile = new TempFile(); + var updater = new TestUpdater(_validatorMock.Object, _backupMock.Object, _options, () => Task.CompletedTask); + var driver = CreateDriver("test", tempFile.Path); + var strategy = new UpdateStrategy { RequireBackup = false }; + + var result = await updater.UpdateAsync(driver, strategy); + + Assert.True(result.Success); + Assert.Equal(UpdateStatus.Succeeded, result.Status); + } + + [Fact] + public async Task UpdateAsync_WithProgress_ReportsProgress() + { + using var tempFile = new TempFile(); + var updater = new TestUpdater(_validatorMock.Object, _backupMock.Object, _options, () => Task.CompletedTask); + var driver = CreateDriver("test", tempFile.Path); + var strategy = new UpdateStrategy { RequireBackup = false }; + + var progressItems = new List(); + var progress = new Progress(p => progressItems.Add(p)); + + await updater.UpdateAsync(driver, strategy, progress); + + Assert.NotEmpty(progressItems); + Assert.Contains(progressItems, p => p.Percentage == 100); + } + + [Fact] + public async Task UpdateAsync_WithEvents_RaisesEvents() + { + using var tempFile = new TempFile(); + var updater = new TestUpdater(_validatorMock.Object, _backupMock.Object, _options, () => Task.CompletedTask); + var driver = CreateDriver("test", tempFile.Path); + var strategy = new UpdateStrategy { RequireBackup = false }; + + var stepStarted = new List(); + var stepCompleted = new List(); + UpdateResult? completedResult = null; + + updater.OnStepStarted += s => stepStarted.Add(s); + updater.OnStepCompleted += s => stepCompleted.Add(s); + updater.OnUpdateCompleted += r => completedResult = r; + + await updater.UpdateAsync(driver, strategy); + + Assert.NotEmpty(stepStarted); + Assert.NotEmpty(stepCompleted); + Assert.NotNull(completedResult); + } + + [Fact] + public async Task UpdateAsync_WithTimeout_ReportsTimeout() + { + // Set a very short timeout + var options = new DrivelutionOptions { DefaultTimeoutSeconds = 1 }; + var slowUpdater = new TestUpdater(_validatorMock.Object, _backupMock.Object, options, + (Func)(async ct => await Task.Delay(5000, ct))); + + using var tempFile = new TempFile(); + var driver = CreateDriver("test", tempFile.Path); + var strategy = new UpdateStrategy { RequireBackup = false, TimeoutSeconds = 1 }; + + var result = await slowUpdater.UpdateAsync(driver, strategy); + + Assert.False(result.Success); + Assert.Equal(ErrorType.Timeout, result.Error?.Type); + } + + [Fact] + public async Task UpdateAsync_WithBackup_BacksUpDriver() + { + using var tempFile = new TempFile(); + var updater = new TestUpdater(_validatorMock.Object, _backupMock.Object, _options, () => Task.CompletedTask); + var driver = CreateDriver("test", tempFile.Path); + var strategy = new UpdateStrategy { RequireBackup = true, BackupPath = "./backups" }; + + var result = await updater.UpdateAsync(driver, strategy); + + _backupMock.Verify(b => b.BackupAsync(driver.FilePath, It.IsAny(), It.IsAny()), Times.Once); + Assert.True(result.Success); + } + + [Fact] + public async Task ValidateAsync_WithInvalidFile_ReturnsFalse() + { + var updater = new TestUpdater(_validatorMock.Object, _backupMock.Object, _options, () => Task.CompletedTask); + var driver = new DriverInfo { Name = "Test", FilePath = "/nonexistent/file.sys" }; + + var result = await updater.ValidateAsync(driver); + Assert.False(result); + } + + [Fact] + public async Task ValidateAsync_WithHashMismatch_ReturnsFalse() + { + _validatorMock.Setup(v => v.ValidateIntegrityAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(false); + _validatorMock.Setup(v => v.ValidateCompatibilityAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + using var tempFile = new TempFile(); + var updater = new TestUpdater(_validatorMock.Object, _backupMock.Object, _options, () => Task.CompletedTask); + var driver = CreateDriver("test", tempFile.Path); + driver.Hash = "bogus-hash"; + + var result = await updater.ValidateAsync(driver); + Assert.False(result); + } + + [Fact] + public async Task BackupAsync_ForwardsToBackupService() + { + using var tempFile = new TempFile(); + var updater = new TestUpdater(_validatorMock.Object, _backupMock.Object, _options, () => Task.CompletedTask); + var driver = CreateDriver("test", tempFile.Path); + + await updater.BackupAsync(driver, "/backups/test"); + + _backupMock.Verify(b => b.BackupAsync(driver.FilePath, "/backups/test", It.IsAny()), Times.Once); + } + + [Fact] + public async Task BatchUpdateAsync_Sequential_ProcessesAllDrivers() + { + using var tempFile1 = new TempFile(); + using var tempFile2 = new TempFile(); + using var tempFile3 = new TempFile(); + var updater = new TestUpdater(_validatorMock.Object, _backupMock.Object, _options, () => Task.CompletedTask); + var drivers = new[] + { + CreateDriver("drv1", tempFile1.Path), + CreateDriver("drv2", tempFile2.Path), + CreateDriver("drv3", tempFile3.Path) + }; + var strategy = new UpdateStrategy { RequireBackup = false }; + + var result = await updater.BatchUpdateAsync(drivers, strategy, BatchMode.Sequential); + + Assert.True(result.AllSucceeded); + Assert.Equal(3, result.SucceededCount); + Assert.Equal(0, result.FailedCount); + Assert.Equal(3, result.Results.Count); + Assert.True(result.Duration > TimeSpan.Zero); + } + + [Fact] + public async Task BatchUpdateAsync_Parallel_ProcessesAllDrivers() + { + using var tempFile1 = new TempFile(); + using var tempFile2 = new TempFile(); + var updater = new TestUpdater(_validatorMock.Object, _backupMock.Object, _options, () => Task.CompletedTask); + var drivers = new[] + { + CreateDriver("drv1", tempFile1.Path), + CreateDriver("drv2", tempFile2.Path) + }; + var strategy = new UpdateStrategy { RequireBackup = false }; + + var result = await updater.BatchUpdateAsync(drivers, strategy, BatchMode.Parallel); + + Assert.True(result.AllSucceeded); + Assert.Equal(2, result.SucceededCount); + } + + [Fact] + public async Task BatchUpdateAsync_WithProgress_ReportsProgress() + { + using var tempFile = new TempFile(); + var updater = new TestUpdater(_validatorMock.Object, _backupMock.Object, _options, () => Task.CompletedTask); + var drivers = new[] { CreateDriver("drv1", tempFile.Path) }; + var strategy = new UpdateStrategy { RequireBackup = false }; + var progressItems = new List(); + var progress = new Progress(p => progressItems.Add(p)); + + await updater.BatchUpdateAsync(drivers, strategy, BatchMode.Sequential, progress); + + Assert.NotEmpty(progressItems); + Assert.Contains(progressItems, p => p.Percentage == 100); + } + + private static DriverInfo CreateDriver(string name, string filePath) => new() + { + Name = name, + Version = "1.0.0", + FilePath = filePath + }; + + /// + /// Minimal concrete subclass for testing BaseDriverUpdater. + /// + private class TestUpdater : BaseDriverUpdater + { + private readonly Func _installAction; + + public TestUpdater( + IDriverValidator validator, + IDriverBackup backup, + DrivelutionOptions? options, + Func installAction) + : this(validator, backup, options, _ => installAction()) + { } + + public TestUpdater( + IDriverValidator validator, + IDriverBackup backup, + DrivelutionOptions? options, + Func installAction) + : base(validator, backup, options) + { + _installAction = installAction; + } + + protected override Task InstallCoreAsync(DriverInfo driverInfo, UpdateStrategy strategy, CancellationToken cancellationToken) + => _installAction(cancellationToken); + } + + /// + /// Creates a temp file that is deleted on disposal. + /// + private sealed class TempFile : IDisposable + { + public string Path { get; } + + public TempFile() + { + Path = System.IO.Path.GetTempFileName(); + File.WriteAllText(Path, "test content"); + } + + public void Dispose() + { + try { File.Delete(Path); } catch { } + } + } +} diff --git a/src/c#/DrivelutionTest/Pipeline/PipelineContextTests.cs b/src/c#/DrivelutionTest/Pipeline/PipelineContextTests.cs new file mode 100644 index 00000000..9ad00108 --- /dev/null +++ b/src/c#/DrivelutionTest/Pipeline/PipelineContextTests.cs @@ -0,0 +1,48 @@ +using GeneralUpdate.Drivelution.Abstractions.Models; +using GeneralUpdate.Drivelution.Core.Pipeline; + +namespace DrivelutionTest.Pipeline; + +public class PipelineContextTests +{ + [Fact] + public void Constructor_InitializesAllProperties() + { + var driverInfo = new DriverInfo { Name = "test" }; + var strategy = new UpdateStrategy(); + var result = new UpdateResult(); + + var ctx = new PipelineContext(driverInfo, strategy, result); + + Assert.Same(driverInfo, ctx.DriverInfo); + Assert.Same(strategy, ctx.Strategy); + Assert.Same(result, ctx.Result); + Assert.NotNull(ctx.Bag); + Assert.Empty(ctx.Bag); + } + + [Fact] + public void Bag_CanStoreAndRetrieveValues() + { + var ctx = CreateContext(); + + ctx.Bag["BackupPath"] = "/tmp/backup"; + ctx.Bag["CustomData"] = 42; + + Assert.Equal("/tmp/backup", ctx.Bag["BackupPath"]); + Assert.Equal(42, ctx.Bag["CustomData"]); + } + + [Fact] + public void Constructor_ThrowsOnNull() + { + Assert.Throws(() => new PipelineContext(null!, new UpdateStrategy(), new UpdateResult())); + Assert.Throws(() => new PipelineContext(new DriverInfo(), null!, new UpdateResult())); + Assert.Throws(() => new PipelineContext(new DriverInfo(), new UpdateStrategy(), null!)); + } + + private static PipelineContext CreateContext() => new( + new DriverInfo { Name = "test" }, + new UpdateStrategy(), + new UpdateResult()); +} diff --git a/src/c#/DrivelutionTest/Pipeline/PipelineResultTests.cs b/src/c#/DrivelutionTest/Pipeline/PipelineResultTests.cs new file mode 100644 index 00000000..cc0c5e49 --- /dev/null +++ b/src/c#/DrivelutionTest/Pipeline/PipelineResultTests.cs @@ -0,0 +1,31 @@ +using GeneralUpdate.Drivelution.Core.Pipeline; + +namespace DrivelutionTest.Pipeline; + +public class PipelineResultTests +{ + [Fact] + public void Ok_ReturnsSuccessfulResult() + { + var result = PipelineResult.Ok(); + Assert.True(result.Success); + Assert.Null(result.ErrorMessage); + Assert.Null(result.Exception); + } + + [Fact] + public void Fail_ReturnsFailedResult() + { + var result = PipelineResult.Fail("something went wrong"); + Assert.False(result.Success); + Assert.Equal("something went wrong", result.ErrorMessage); + } + + [Fact] + public void Fail_WithException_StoresException() + { + var ex = new InvalidOperationException("test"); + var result = PipelineResult.Fail("error", ex); + Assert.Same(ex, result.Exception); + } +} diff --git a/src/c#/DrivelutionTest/Pipeline/RetryPolicyTests.cs b/src/c#/DrivelutionTest/Pipeline/RetryPolicyTests.cs new file mode 100644 index 00000000..419c6957 --- /dev/null +++ b/src/c#/DrivelutionTest/Pipeline/RetryPolicyTests.cs @@ -0,0 +1,133 @@ +using GeneralUpdate.Drivelution.Core.Pipeline; + +namespace DrivelutionTest.Pipeline; + +public class RetryPolicyTests +{ + [Fact] + public void Default_HasExpectedValues() + { + var policy = RetryPolicy.Default; + Assert.Equal(3, policy.MaxRetries); + Assert.Equal(TimeSpan.FromSeconds(5), policy.Delay); + Assert.False(policy.UseExponentialBackoff); + } + + [Fact] + public void NoRetry_HasZeroMaxRetries() + { + var policy = RetryPolicy.NoRetry; + Assert.Equal(0, policy.MaxRetries); + Assert.Equal(TimeSpan.Zero, policy.Delay); + } + + [Fact] + public async Task ExecuteAsync_SucceedsOnFirstAttempt_ReturnsResult() + { + var policy = RetryPolicy.Default; + int attempts = 0; + + var result = await policy.ExecuteAsync(async ct => + { + attempts++; + await Task.CompletedTask; + return 42; + }); + + Assert.Equal(42, result); + Assert.Equal(1, attempts); + } + + [Fact] + public async Task ExecuteAsync_RetriesOnFailure() + { + var policy = new RetryPolicy(2, TimeSpan.FromMilliseconds(10)); + int attempts = 0; + + var result = await policy.ExecuteAsync(async ct => + { + attempts++; + if (attempts < 3) + throw new InvalidOperationException($"Attempt {attempts}"); + await Task.CompletedTask; + return "success"; + }); + + Assert.Equal("success", result); + Assert.Equal(3, attempts); + } + + [Fact] + public async Task ExecuteAsync_DoesNotRetryOnCancellation() + { + var policy = new RetryPolicy(3, TimeSpan.FromMilliseconds(100)); + int attempts = 0; + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAsync(() => + policy.ExecuteAsync(async ct => + { + attempts++; + ct.ThrowIfCancellationRequested(); + return 0; + }, cts.Token)); + + Assert.Equal(1, attempts); // single attempt, no retry on cancellation + } + + [Fact] + public async Task ExecuteWithRetryAsync_ReturnsFalseAfterExhaustingRetries() + { + var policy = new RetryPolicy(2, TimeSpan.FromMilliseconds(5)); + int attempts = 0; + + var result = await policy.ExecuteWithRetryAsync(async ct => + { + attempts++; + return false; + }); + + Assert.False(result); + Assert.Equal(3, attempts); // 1 initial + 2 retries + } + + [Fact] + public async Task ExecuteWithRetryAsync_ReturnsTrueOnSuccess() + { + var policy = new RetryPolicy(2, TimeSpan.FromMilliseconds(5)); + int attempts = 0; + + var result = await policy.ExecuteWithRetryAsync(async ct => + { + attempts++; + return attempts >= 2; + }); + + Assert.True(result); + Assert.Equal(2, attempts); + } + + [Fact] + public void FromOptions_Null_ReturnsDefault() + { + var policy = RetryPolicy.FromOptions(null); + Assert.Equal(RetryPolicy.Default.MaxRetries, policy.MaxRetries); + } + + [Fact] + public void FromOptions_UsesConfiguredValues() + { + var options = new GeneralUpdate.Drivelution.Abstractions.Configuration.DrivelutionOptions + { + DefaultRetryCount = 5, + DefaultRetryIntervalSeconds = 10, + UseExponentialBackoff = true + }; + + var policy = RetryPolicy.FromOptions(options); + Assert.Equal(5, policy.MaxRetries); + Assert.Equal(TimeSpan.FromSeconds(10), policy.Delay); + Assert.True(policy.UseExponentialBackoff); + } +}