diff --git a/tests/CoreTest/Configuration/BaseConfigInfoTests.cs b/tests/CoreTest/Configuration/BaseConfigInfoTests.cs
new file mode 100644
index 00000000..201e8fb2
--- /dev/null
+++ b/tests/CoreTest/Configuration/BaseConfigInfoTests.cs
@@ -0,0 +1,148 @@
+using GeneralUpdate.Core.Configuration;
+
+namespace CoreTest.Configuration;
+
+///
+/// AAAT unit tests for — default property values.
+/// Covers: all default property values defined in the abstract base class.
+///
+public class BaseConfigInfoTests
+{
+ private class TestableConfig : BaseConfigInfo { }
+
+ [Fact]
+ public void Ctor_AppName_DefaultsToUpdateExe()
+ {
+ var config = new TestableConfig();
+ Assert.Equal("Update.exe", config.AppName);
+ }
+
+ [Fact]
+ public void Ctor_MainAppName_DefaultsToNull()
+ {
+ var config = new TestableConfig();
+ Assert.Null(config.MainAppName);
+ }
+
+ [Fact]
+ public void Ctor_InstallPath_DefaultsToBaseDirectory()
+ {
+ var config = new TestableConfig();
+ var expected = AppDomain.CurrentDomain.BaseDirectory;
+ Assert.Equal(expected, config.InstallPath);
+ }
+
+ [Fact]
+ public void Ctor_UpdateLogUrl_DefaultsToNull()
+ {
+ var config = new TestableConfig();
+ Assert.Null(config.UpdateLogUrl);
+ }
+
+ [Fact]
+ public void Ctor_AppSecretKey_DefaultsToNull()
+ {
+ var config = new TestableConfig();
+ Assert.Null(config.AppSecretKey);
+ }
+
+ [Fact]
+ public void Ctor_ClientVersion_DefaultsToNull()
+ {
+ var config = new TestableConfig();
+ Assert.Null(config.ClientVersion);
+ }
+
+ [Fact]
+ public void Ctor_BlackFiles_DefaultsToNull()
+ {
+ var config = new TestableConfig();
+ Assert.Null(config.BlackFiles);
+ }
+
+ [Fact]
+ public void Ctor_BlackFormats_DefaultsToNull()
+ {
+ var config = new TestableConfig();
+ Assert.Null(config.BlackFormats);
+ }
+
+ [Fact]
+ public void Ctor_SkipDirectorys_DefaultsToNull()
+ {
+ var config = new TestableConfig();
+ Assert.Null(config.SkipDirectorys);
+ }
+
+ [Fact]
+ public void Ctor_ReportUrl_DefaultsToNull()
+ {
+ var config = new TestableConfig();
+ Assert.Null(config.ReportUrl);
+ }
+
+ [Fact]
+ public void Ctor_Bowl_DefaultsToNull()
+ {
+ var config = new TestableConfig();
+ Assert.Null(config.Bowl);
+ }
+
+ [Fact]
+ public void Ctor_Scheme_DefaultsToNull()
+ {
+ var config = new TestableConfig();
+ Assert.Null(config.Scheme);
+ }
+
+ [Fact]
+ public void Ctor_Token_DefaultsToNull()
+ {
+ var config = new TestableConfig();
+ Assert.Null(config.Token);
+ }
+
+ [Fact]
+ public void Ctor_DriverDirectory_DefaultsToNull()
+ {
+ var config = new TestableConfig();
+ Assert.Null(config.DriverDirectory);
+ }
+
+ [Fact]
+ public void AllProperties_CanBeSetAndGet()
+ {
+ var config = new TestableConfig
+ {
+ AppName = "MyApp.exe",
+ MainAppName = "MainApp",
+ InstallPath = "C:\\MyApp",
+ UpdateLogUrl = "https://logs.example.com",
+ AppSecretKey = "secret-key",
+ ClientVersion = "1.2.3",
+ BlackFiles = new List { "a.dll" },
+ BlackFormats = new List { ".log" },
+ SkipDirectorys = new List { "temp" },
+ ReportUrl = "https://report.example.com",
+ Bowl = "Bowl.exe",
+ Scheme = "https",
+ Token = "bearer-token",
+ DriverDirectory = "C:\\Drivers"
+ };
+
+ Assert.Equal("MyApp.exe", config.AppName);
+ Assert.Equal("MainApp", config.MainAppName);
+ Assert.Equal("C:\\MyApp", config.InstallPath);
+ Assert.Equal("https://logs.example.com", config.UpdateLogUrl);
+ Assert.Equal("secret-key", config.AppSecretKey);
+ Assert.Equal("1.2.3", config.ClientVersion);
+ Assert.Single(config.BlackFiles);
+ Assert.Single(config.BlackFormats);
+ Assert.Single(config.SkipDirectorys);
+ Assert.Equal("https://report.example.com", config.ReportUrl);
+ Assert.Equal("Bowl.exe", config.Bowl);
+ Assert.Equal("https", config.Scheme);
+ Assert.Equal("bearer-token", config.Token);
+ Assert.Equal("C:\\Drivers", config.DriverDirectory);
+ }
+}
diff --git a/tests/CoreTest/Configuration/BlackListConfigBuilderTests.cs b/tests/CoreTest/Configuration/BlackListConfigBuilderTests.cs
new file mode 100644
index 00000000..a260c694
--- /dev/null
+++ b/tests/CoreTest/Configuration/BlackListConfigBuilderTests.cs
@@ -0,0 +1,240 @@
+using GeneralUpdate.Core.Configuration;
+using GeneralUpdate.Core.FileSystem;
+
+namespace CoreTest.Configuration;
+
+///
+/// AAAT unit tests for and .
+/// Covers: fluent chaining, empty builds, null-vs-empty discrimination, HasRules logic, Builder reuse.
+///
+public class BlackListConfigBuilderTests
+{
+ #region BlackListConfigBuilder fluent API
+
+ [Fact]
+ public void Build_NoMethodsCalled_AllPropertiesNull()
+ {
+ // Arrange
+ var builder = new BlackListConfigBuilder();
+
+ // Act
+ var config = builder.Build();
+
+ // Assert
+ Assert.Null(config.BlackFiles);
+ Assert.Null(config.BlackFormats);
+ Assert.Null(config.SkipDirectorys);
+ }
+
+ [Fact]
+ public void Build_OnlyAddBlackFiles_OthersNull()
+ {
+ // Arrange
+ var builder = new BlackListConfigBuilder();
+ builder.AddBlackFiles("a.dll", "b.dll");
+
+ // Act
+ var config = builder.Build();
+
+ // Assert
+ Assert.NotNull(config.BlackFiles);
+ Assert.Equal(2, config.BlackFiles!.Count);
+ Assert.Contains("a.dll", config.BlackFiles);
+ Assert.Contains("b.dll", config.BlackFiles);
+ Assert.Null(config.BlackFormats);
+ Assert.Null(config.SkipDirectorys);
+ }
+
+ [Fact]
+ public void Build_OnlyAddBlackFormats_OthersNull()
+ {
+ var builder = new BlackListConfigBuilder();
+ builder.AddBlackFormats(".patch", ".pdb", ".json");
+
+ var config = builder.Build();
+
+ Assert.NotNull(config.BlackFormats);
+ Assert.Equal(3, config.BlackFormats!.Count);
+ Assert.Null(config.BlackFiles);
+ Assert.Null(config.SkipDirectorys);
+ }
+
+ [Fact]
+ public void Build_OnlyAddSkipDirectories_OthersNull()
+ {
+ var builder = new BlackListConfigBuilder();
+ builder.AddSkipDirectories("app-", "fail");
+
+ var config = builder.Build();
+
+ Assert.NotNull(config.SkipDirectorys);
+ Assert.Equal(2, config.SkipDirectorys!.Count);
+ Assert.Null(config.BlackFiles);
+ Assert.Null(config.BlackFormats);
+ }
+
+ [Fact]
+ public void Build_AllThreeSectionsFilled_AllReturned()
+ {
+ var builder = new BlackListConfigBuilder();
+ builder.AddBlackFiles("f1.dll");
+ builder.AddBlackFormats(".log");
+ builder.AddSkipDirectories("tmp");
+
+ var config = builder.Build();
+
+ Assert.Single(config.BlackFiles!);
+ Assert.Single(config.BlackFormats!);
+ Assert.Single(config.SkipDirectorys!);
+ }
+
+ [Fact]
+ public void AddBlackFiles_EmptyParams_NoItemsAdded()
+ {
+ var builder = new BlackListConfigBuilder();
+ builder.AddBlackFiles();
+
+ var config = builder.Build();
+
+ Assert.Null(config.BlackFiles);
+ }
+
+ [Fact]
+ public void AddBlackFormats_EmptyParams_NoItemsAdded()
+ {
+ var builder = new BlackListConfigBuilder();
+ builder.AddBlackFormats();
+
+ var config = builder.Build();
+
+ Assert.Null(config.BlackFormats);
+ }
+
+ [Fact]
+ public void AddSkipDirectories_EmptyParams_NoItemsAdded()
+ {
+ var builder = new BlackListConfigBuilder();
+ builder.AddSkipDirectories();
+
+ var config = builder.Build();
+
+ Assert.Null(config.SkipDirectorys);
+ }
+
+ [Fact]
+ public void AddBlackFiles_MultipleCalls_Accumulates()
+ {
+ var builder = new BlackListConfigBuilder();
+ builder.AddBlackFiles("a.dll");
+ builder.AddBlackFiles("b.dll", "c.dll");
+
+ var config = builder.Build();
+
+ Assert.Equal(3, config.BlackFiles!.Count);
+ }
+
+ [Fact]
+ public void AddBlackFormats_MultipleCalls_Accumulates()
+ {
+ var builder = new BlackListConfigBuilder();
+ builder.AddBlackFormats(".dll");
+ builder.AddBlackFormats(".exe", ".log");
+
+ var config = builder.Build();
+
+ Assert.Equal(3, config.BlackFormats!.Count);
+ }
+
+ [Fact]
+ public void AddSkipDirectories_MultipleCalls_Accumulates()
+ {
+ var builder = new BlackListConfigBuilder();
+ builder.AddSkipDirectories("app-");
+ builder.AddSkipDirectories("temp", "cache");
+
+ var config = builder.Build();
+
+ Assert.Equal(3, config.SkipDirectorys!.Count);
+ }
+
+ [Fact]
+ public void Builder_FluentChaining_ReturnsBuilder()
+ {
+ var builder = new BlackListConfigBuilder();
+ var result = builder.AddBlackFiles("a").AddBlackFormats(".z").AddSkipDirectories("d");
+
+ Assert.Same(builder, result);
+ }
+
+ [Fact]
+ public void Build_ProducesNonNullLists()
+ {
+ var builder = new BlackListConfigBuilder();
+ builder.AddBlackFiles("f.dll");
+
+ var config = builder.Build();
+
+ Assert.NotNull(config.BlackFiles);
+ Assert.Single(config.BlackFiles);
+ }
+
+ #endregion
+
+ #region BlackListConfig
+
+ [Fact]
+ public void Empty_HasNoRules()
+ {
+ var empty = BlackListConfig.Empty;
+
+ Assert.False(empty.HasRules);
+ Assert.Null(empty.BlackFiles);
+ Assert.Null(empty.BlackFormats);
+ Assert.Null(empty.SkipDirectorys);
+ }
+
+ [Theory]
+ // All 8 combinations of (hasFiles, hasFormats, hasDirs)
+ [InlineData(false, false, false, false)] // none → false
+ [InlineData(true, false, false, true)] // files only → true
+ [InlineData(false, true, false, true)] // formats only → true
+ [InlineData(false, false, true, true)] // dirs only → true
+ [InlineData(true, true, false, true)] // files + formats → true
+ [InlineData(true, false, true, true)] // files + dirs → true
+ [InlineData(false, true, true, true)] // formats + dirs → true
+ [InlineData(true, true, true, true)] // all three → true
+ public void HasRules_AllCombinations(bool hasFiles, bool hasFormats, bool hasDirs, bool expected)
+ {
+ var builder = new BlackListConfigBuilder();
+ if (hasFiles) builder.AddBlackFiles("f.dll");
+ if (hasFormats) builder.AddBlackFormats(".log");
+ if (hasDirs) builder.AddSkipDirectories("tmp");
+
+ var config = builder.Build();
+
+ Assert.Equal(expected, config.HasRules);
+ }
+
+ [Fact]
+ public void HasRules_AllNull_ReturnsFalse()
+ {
+ var config = new BlackListConfig(null, null, null);
+
+ Assert.False(config.HasRules);
+ }
+
+ [Fact]
+ public void HasRules_AllEmptyLists_ReturnsFalse()
+ {
+ // Empty lists differ from null — the builder produces null for empty
+ // Direct record construction with empty lists
+ var config = new BlackListConfig(
+ new List().AsReadOnly(),
+ new List().AsReadOnly(),
+ new List().AsReadOnly());
+
+ Assert.False(config.HasRules);
+ }
+
+ #endregion
+}
diff --git a/tests/CoreTest/Configuration/ComparisonResultTests.cs b/tests/CoreTest/Configuration/ComparisonResultTests.cs
new file mode 100644
index 00000000..680069b7
--- /dev/null
+++ b/tests/CoreTest/Configuration/ComparisonResultTests.cs
@@ -0,0 +1,205 @@
+using GeneralUpdate.Core.FileSystem;
+
+namespace CoreTest.Configuration;
+
+///
+/// AAAT unit tests for .
+/// Covers: default constructor state, AddToLeft/Right/Different, multiple adds, empty adds, immutability via AsReadOnly.
+///
+public class ComparisonResultTests
+{
+ private static FileNode Node(string name) => new() { Name = name, Path = "/root/" + name, Hash = "abc" };
+
+ #region Constructor / Default State
+
+ [Fact]
+ public void Ctor_Default_AllListsEmpty()
+ {
+ var cr = new ComparisonResult();
+
+ Assert.Empty(cr.LeftNodes);
+ Assert.Empty(cr.RightNodes);
+ Assert.Empty(cr.DifferentNodes);
+ }
+
+ [Fact]
+ public void Ctor_Default_AllListsNotNull()
+ {
+ var cr = new ComparisonResult();
+
+ Assert.NotNull(cr.LeftNodes);
+ Assert.NotNull(cr.RightNodes);
+ Assert.NotNull(cr.DifferentNodes);
+ }
+
+ #endregion
+
+ #region AddToLeft
+
+ [Fact]
+ public void AddToLeft_SingleNode_AddedToLeft()
+ {
+ var cr = new ComparisonResult();
+ var node = Node("a.txt");
+
+ cr.AddToLeft(new[] { node });
+
+ Assert.Single(cr.LeftNodes);
+ Assert.Equal("a.txt", cr.LeftNodes[0].Name);
+ }
+
+ [Fact]
+ public void AddToLeft_MultipleCalls_Accumulates()
+ {
+ var cr = new ComparisonResult();
+
+ cr.AddToLeft(new[] { Node("a.txt") });
+ cr.AddToLeft(new[] { Node("b.txt"), Node("c.txt") });
+
+ Assert.Equal(3, cr.LeftNodes.Count);
+ }
+
+ [Fact]
+ public void AddToLeft_EmptyEnumerable_CountUnchanged()
+ {
+ var cr = new ComparisonResult();
+ cr.AddToLeft(Enumerable.Empty());
+
+ Assert.Empty(cr.LeftNodes);
+ }
+
+ [Fact]
+ public void AddToLeft_DoesNotAffectRightOrDifferent()
+ {
+ var cr = new ComparisonResult();
+ cr.AddToLeft(new[] { Node("a.txt") });
+
+ Assert.Empty(cr.RightNodes);
+ Assert.Empty(cr.DifferentNodes);
+ }
+
+ #endregion
+
+ #region AddToRight
+
+ [Fact]
+ public void AddToRight_SingleNode_AddedToRight()
+ {
+ var cr = new ComparisonResult();
+ var node = Node("b.txt");
+
+ cr.AddToRight(new[] { node });
+
+ Assert.Single(cr.RightNodes);
+ Assert.Equal("b.txt", cr.RightNodes[0].Name);
+ }
+
+ [Fact]
+ public void AddToRight_MultipleCalls_Accumulates()
+ {
+ var cr = new ComparisonResult();
+
+ cr.AddToRight(new[] { Node("x.txt") });
+ cr.AddToRight(new[] { Node("y.txt") });
+
+ Assert.Equal(2, cr.RightNodes.Count);
+ }
+
+ [Fact]
+ public void AddToRight_DoesNotAffectLeftOrDifferent()
+ {
+ var cr = new ComparisonResult();
+ cr.AddToRight(new[] { Node("b.txt") });
+
+ Assert.Empty(cr.LeftNodes);
+ Assert.Empty(cr.DifferentNodes);
+ }
+
+ #endregion
+
+ #region AddDifferent
+
+ [Fact]
+ public void AddDifferent_SingleNode_AddedToDifferent()
+ {
+ var cr = new ComparisonResult();
+ var node = Node("c.txt");
+
+ cr.AddDifferent(new[] { node });
+
+ Assert.Single(cr.DifferentNodes);
+ Assert.Equal("c.txt", cr.DifferentNodes[0].Name);
+ }
+
+ [Fact]
+ public void AddDifferent_MultipleCalls_Accumulates()
+ {
+ var cr = new ComparisonResult();
+
+ cr.AddDifferent(new[] { Node("m.txt"), Node("n.txt") });
+ cr.AddDifferent(new[] { Node("o.txt") });
+
+ Assert.Equal(3, cr.DifferentNodes.Count);
+ }
+
+ [Fact]
+ public void AddDifferent_DoesNotAffectLeftOrRight()
+ {
+ var cr = new ComparisonResult();
+ cr.AddDifferent(new[] { Node("d.txt") });
+
+ Assert.Empty(cr.LeftNodes);
+ Assert.Empty(cr.RightNodes);
+ }
+
+ #endregion
+
+ #region Combined usage
+
+ [Fact]
+ public void AllThreeSections_CanBePopulatedSimultaneously()
+ {
+ var cr = new ComparisonResult();
+
+ cr.AddToLeft(new[] { Node("only-left.txt") });
+ cr.AddToRight(new[] { Node("only-right.txt") });
+ cr.AddDifferent(new[] { Node("changed.txt"), Node("also-changed.txt") });
+
+ Assert.Single(cr.LeftNodes);
+ Assert.Single(cr.RightNodes);
+ Assert.Equal(2, cr.DifferentNodes.Count);
+ }
+
+ #endregion
+
+ #region ReadOnly properties
+
+ [Fact]
+ public void LeftNodes_IsReadOnly()
+ {
+ var cr = new ComparisonResult();
+ cr.AddToLeft(new[] { Node("a.txt") });
+
+ Assert.True(((System.Collections.IList)cr.LeftNodes).IsReadOnly);
+ }
+
+ [Fact]
+ public void RightNodes_IsReadOnly()
+ {
+ var cr = new ComparisonResult();
+ cr.AddToRight(new[] { Node("b.txt") });
+
+ Assert.True(((System.Collections.IList)cr.RightNodes).IsReadOnly);
+ }
+
+ [Fact]
+ public void DifferentNodes_IsReadOnly()
+ {
+ var cr = new ComparisonResult();
+ cr.AddDifferent(new[] { Node("c.txt") });
+
+ Assert.True(((System.Collections.IList)cr.DifferentNodes).IsReadOnly);
+ }
+
+ #endregion
+}
diff --git a/tests/CoreTest/Configuration/ConfigurationMapperExtendedTests.cs b/tests/CoreTest/Configuration/ConfigurationMapperExtendedTests.cs
new file mode 100644
index 00000000..de52d9c2
--- /dev/null
+++ b/tests/CoreTest/Configuration/ConfigurationMapperExtendedTests.cs
@@ -0,0 +1,233 @@
+namespace CoreTest.Configuration;
+
+using GeneralUpdate.Core.Configuration;
+
+///
+/// AAAT unit tests for — additional edge case coverage beyond existing tests.
+/// Covers: MapToProcessInfo required fields, MapToGlobalConfigInfo edge fields, CopyBaseFields cross-type.
+///
+public class ConfigurationMapperExtendedTests : IDisposable
+{
+ private readonly string _tempInstallDir;
+
+ public ConfigurationMapperExtendedTests()
+ {
+ _tempInstallDir = Path.Combine(Path.GetTempPath(), "MapToProcessInfo_" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(_tempInstallDir);
+ }
+
+ public void Dispose()
+ {
+ try { if (Directory.Exists(_tempInstallDir)) Directory.Delete(_tempInstallDir, true); } catch { }
+ }
+
+ /// Creates a valid GlobalConfigInfo with all required fields for MapToProcessInfo.
+ private GlobalConfigInfo CreateValidSource()
+ {
+ return new GlobalConfigInfo
+ {
+ MainAppName = "MainApp",
+ InstallPath = _tempInstallDir,
+ ClientVersion = "1.0.0",
+ LastVersion = "2.0.0",
+ Encoding = System.Text.Encoding.UTF8,
+ Format = ".zip",
+ AppSecretKey = "secret",
+ ReportUrl = "https://report.example.com",
+ BackupDirectory = Path.Combine(_tempInstallDir, "backup")
+ };
+ }
+
+ private static List OneVersion(string ver = "2.0.0")
+ => new() { new VersionInfo { Version = ver, Hash = "abc", Name = $"v{ver}.zip" } };
+
+ #region MapToGlobalConfigInfo — additional edge cases
+
+ [Fact]
+ public void MapToGlobalConfigInfo_SourceWithBlackLists_CopiesCorrectly()
+ {
+ var source = new Configinfo
+ {
+ BlackFiles = new List { "a.dll", "b.dll" },
+ BlackFormats = new List { ".log", ".tmp" },
+ SkipDirectorys = new List { "app-1.0", "cache" }
+ };
+
+ var result = ConfigurationMapper.MapToGlobalConfigInfo(source);
+
+ Assert.Equal(2, result.BlackFiles?.Count);
+ Assert.Equal(2, result.BlackFormats?.Count);
+ Assert.Equal(2, result.SkipDirectorys?.Count);
+ }
+
+ [Fact]
+ public void MapToGlobalConfigInfo_SourceWithNullLists_ReturnsNullLists()
+ {
+ var source = new Configinfo { BlackFiles = null, BlackFormats = null, SkipDirectorys = null };
+
+ var result = ConfigurationMapper.MapToGlobalConfigInfo(source);
+
+ Assert.Null(result.BlackFiles);
+ Assert.Null(result.BlackFormats);
+ Assert.Null(result.SkipDirectorys);
+ }
+
+ [Fact]
+ public void MapToGlobalConfigInfo_PreservesExistingTargetFieldsNotInSource()
+ {
+ var target = new GlobalConfigInfo
+ {
+ TempPath = "/custom/temp",
+ BackupDirectory = "/custom/backup",
+ MaxConcurrency = 8
+ };
+ var source = new Configinfo { AppName = "NewApp.exe" };
+
+ var result = ConfigurationMapper.MapToGlobalConfigInfo(source, target);
+
+ Assert.Equal("/custom/temp", result.TempPath);
+ Assert.Equal("/custom/backup", result.BackupDirectory);
+ Assert.Equal(8, result.MaxConcurrency);
+ Assert.Equal("NewApp.exe", result.AppName);
+ }
+
+ #endregion
+
+ #region MapToProcessInfo — valid path scenarios
+
+ [Fact]
+ public void MapToProcessInfo_WithVersionList_CopiesVersions()
+ {
+ var source = CreateValidSource();
+ var versions = new List
+ {
+ new() { Version = "2.0.0", Hash = "abc", Name = "v2.zip" },
+ new() { Version = "3.0.0", Hash = "def", Name = "v3.zip" }
+ };
+
+ var result = ConfigurationMapper.MapToProcessInfo(source, versions,
+ new List(), new List(), new List());
+
+ Assert.Equal(2, result.UpdateVersions.Count);
+ Assert.Equal("2.0.0", result.UpdateVersions[0].Version);
+ Assert.Equal("3.0.0", result.UpdateVersions[1].Version);
+ }
+
+ [Fact]
+ public void MapToProcessInfo_WithBlackAndSkipPaths_ListsSet()
+ {
+ var source = CreateValidSource();
+ var blackFiles = new List { "b1.dll" };
+ var blackFormats = new List { ".log" };
+ var skipDirs = new List { "temp" };
+
+ var result = ConfigurationMapper.MapToProcessInfo(source,
+ OneVersion(), blackFiles, blackFormats, skipDirs);
+
+ // All blacklist lists should be set and non-null
+ Assert.NotNull(result.BlackFiles);
+ Assert.NotNull(result.BlackFileFormats);
+ Assert.NotNull(result.SkipDirectorys);
+ }
+
+ [Fact]
+ public void MapToProcessInfo_WithDriverDirectory_Works()
+ {
+ var source = CreateValidSource();
+ source.DriverDirectory = "C:\\Drivers\\Special";
+
+ var result = ConfigurationMapper.MapToProcessInfo(source,
+ OneVersion(), new List(), new List(), new List());
+
+ Assert.Equal("C:\\Drivers\\Special", result.DriverDirectory);
+ }
+
+ [Fact]
+ public void MapToProcessInfo_StandardConfig_CheckAllMappedProperties()
+ {
+ var source = CreateValidSource();
+ source.UpdateLogUrl = "https://logs.test.com";
+ source.Encoding = System.Text.Encoding.ASCII;
+ source.Format = ".tar";
+ source.DownloadTimeOut = 120;
+
+ var result = ConfigurationMapper.MapToProcessInfo(source,
+ OneVersion(), new List(), new List(), new List());
+
+ Assert.Equal("MainApp", result.AppName); // MainAppName -> AppName
+ Assert.Equal(source.InstallPath, result.InstallPath);
+ Assert.Equal("1.0.0", result.CurrentVersion);
+ Assert.Equal("2.0.0", result.LastVersion);
+ Assert.Equal("secret", result.AppSecretKey);
+ Assert.Equal("us-ascii", result.CompressEncoding);
+ Assert.Equal(".tar", result.CompressFormat);
+ Assert.Equal(120, result.DownloadTimeOut);
+ Assert.Equal("https://logs.test.com", result.UpdateLogUrl);
+ Assert.Equal("https://report.example.com", result.ReportUrl);
+ Assert.NotNull(result.BackupDirectory);
+ }
+
+ #endregion
+
+ #region CopyBaseFields — cross-type between BaseConfigInfo subtypes
+
+ [Fact]
+ public void CopyBaseFields_ConfiginfoToGlobalConfigInfo_Works()
+ {
+ var source = new Configinfo
+ {
+ AppName = "App.exe",
+ MainAppName = "Main",
+ InstallPath = "C:\\app",
+ ClientVersion = "v1",
+ AppSecretKey = "key1",
+ Token = "tok",
+ Bowl = "bowl.exe",
+ DriverDirectory = "C:\\drv"
+ };
+ var target = new GlobalConfigInfo();
+
+ ConfigurationMapper.CopyBaseFields(source, target);
+
+ Assert.Equal("App.exe", target.AppName);
+ Assert.Equal("Main", target.MainAppName);
+ Assert.Equal("C:\\app", target.InstallPath);
+ Assert.Equal("v1", target.ClientVersion);
+ Assert.Equal("key1", target.AppSecretKey);
+ Assert.Equal("tok", target.Token);
+ Assert.Equal("bowl.exe", target.Bowl);
+ Assert.Equal("C:\\drv", target.DriverDirectory);
+ }
+
+ [Fact]
+ public void CopyBaseFields_GraphCopy_ConfiginfoToNewConfiginfo_Works()
+ {
+ var source = new Configinfo
+ {
+ AppName = "Source.exe",
+ ClientVersion = "5.0.0",
+ Scheme = "https"
+ };
+ var target = new Configinfo();
+
+ ConfigurationMapper.CopyBaseFields(source, target);
+
+ Assert.Equal("Source.exe", target.AppName);
+ Assert.Equal("5.0.0", target.ClientVersion);
+ Assert.Equal("https", target.Scheme);
+ }
+
+ [Fact]
+ public void CopyBaseFields_NullSource_DoesNotThrow()
+ {
+ var target = new GlobalConfigInfo { AppName = "keep" };
+
+ var ex = Record.Exception(() =>
+ ConfigurationMapper.CopyBaseFields(null!, target));
+
+ Assert.Null(ex);
+ Assert.Equal("keep", target.AppName);
+ }
+
+ #endregion
+}
diff --git a/tests/CoreTest/Configuration/HubConfigTests.cs b/tests/CoreTest/Configuration/HubConfigTests.cs
new file mode 100644
index 00000000..41304990
--- /dev/null
+++ b/tests/CoreTest/Configuration/HubConfigTests.cs
@@ -0,0 +1,70 @@
+using GeneralUpdate.Core.Configuration;
+
+namespace CoreTest.Configuration;
+
+///
+/// AAAT unit tests for — default values and property mutation.
+/// Covers: default construction, property get/set, boundary time spans, negative reconnects.
+///
+public class HubConfigTests
+{
+ [Fact]
+ public void Ctor_Default_UrlIsEmpty()
+ {
+ var config = new HubConfig();
+ Assert.Equal(string.Empty, config.Url);
+ }
+
+ [Fact]
+ public void Ctor_Default_ReconnectDelayIs5Seconds()
+ {
+ var config = new HubConfig();
+ Assert.Equal(TimeSpan.FromSeconds(5), config.ReconnectDelay);
+ }
+
+ [Fact]
+ public void Ctor_Default_MaxReconnectAttemptsIs10()
+ {
+ var config = new HubConfig();
+ Assert.Equal(10, config.MaxReconnectAttempts);
+ }
+
+ [Fact]
+ public void Url_SetAndGet_Works()
+ {
+ var config = new HubConfig { Url = "https://hub.example.com/update" };
+ Assert.Equal("https://hub.example.com/update", config.Url);
+ }
+
+ [Fact]
+ public void ReconnectDelay_SetToZero_Works()
+ {
+ var config = new HubConfig { ReconnectDelay = TimeSpan.Zero };
+ Assert.Equal(TimeSpan.Zero, config.ReconnectDelay);
+ }
+
+ [Fact]
+ public void ReconnectDelay_SetToMaxValue_Works()
+ {
+ var config = new HubConfig { ReconnectDelay = TimeSpan.MaxValue };
+ Assert.Equal(TimeSpan.MaxValue, config.ReconnectDelay);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(100)]
+ [InlineData(-1)]
+ public void MaxReconnectAttempts_SetVarious_Works(int attempts)
+ {
+ var config = new HubConfig { MaxReconnectAttempts = attempts };
+ Assert.Equal(attempts, config.MaxReconnectAttempts);
+ }
+
+ [Fact]
+ public void MaxReconnectAttempts_IntMinValue_Works()
+ {
+ var config = new HubConfig { MaxReconnectAttempts = int.MinValue };
+ Assert.Equal(int.MinValue, config.MaxReconnectAttempts);
+ }
+}
diff --git a/tests/CoreTest/Configuration/PacketTests.cs b/tests/CoreTest/Configuration/PacketTests.cs
new file mode 100644
index 00000000..015d0662
--- /dev/null
+++ b/tests/CoreTest/Configuration/PacketTests.cs
@@ -0,0 +1,118 @@
+using GeneralUpdate.Core.Configuration;
+
+namespace CoreTest.Configuration;
+
+///
+/// AAAT unit tests for — property defaults, set/get, JSON serialization attrs.
+/// Covers: all nullable bool?/int?/DateTime?/string? properties, null vs non-null, IsForcibly/IsFreeze tri-state.
+///
+public class PacketTests
+{
+ [Fact]
+ public void Ctor_Default_AllPropertiesAreNullOrDefault()
+ {
+ var packet = new Packet();
+
+ Assert.Null(packet.Name);
+ Assert.Null(packet.Hash);
+ Assert.Null(packet.ReleaseDate);
+ Assert.Null(packet.Url);
+ Assert.Null(packet.Version);
+ Assert.Null(packet.AppType);
+ Assert.Null(packet.Platform);
+ Assert.Null(packet.ProductId);
+ Assert.Null(packet.IsForcibly);
+ Assert.Null(packet.IsFreeze);
+ }
+
+ [Fact]
+ public void FullAssignment_AllPropertiesSet()
+ {
+ var releaseDate = new DateTime(2024, 6, 15, 0, 0, 0, DateTimeKind.Utc);
+
+ var packet = new Packet
+ {
+ Name = "UpdatePack",
+ Hash = "abc123",
+ ReleaseDate = releaseDate,
+ Url = "https://cdn.example.com/pack.zip",
+ Version = "2.0.0",
+ AppType = 1,
+ Platform = 0,
+ ProductId = "prod-001",
+ IsForcibly = true,
+ IsFreeze = false
+ };
+
+ Assert.Equal("UpdatePack", packet.Name);
+ Assert.Equal("abc123", packet.Hash);
+ Assert.Equal(releaseDate, packet.ReleaseDate);
+ Assert.Equal("https://cdn.example.com/pack.zip", packet.Url);
+ Assert.Equal("2.0.0", packet.Version);
+ Assert.Equal(1, packet.AppType);
+ Assert.Equal(0, packet.Platform);
+ Assert.Equal("prod-001", packet.ProductId);
+ Assert.True(packet.IsForcibly);
+ Assert.False(packet.IsFreeze);
+ }
+
+ [Fact]
+ public void IsForcibly_SetToNull_ReturnsNull()
+ {
+ var packet = new Packet();
+ Assert.Null(packet.IsForcibly);
+
+ packet.IsForcibly = true;
+ Assert.True(packet.IsForcibly);
+
+ packet.IsForcibly = null;
+ Assert.Null(packet.IsForcibly);
+ }
+
+ [Fact]
+ public void IsFreeze_SetToNull_ReturnsNull()
+ {
+ var packet = new Packet();
+ Assert.Null(packet.IsFreeze);
+
+ packet.IsFreeze = false;
+ Assert.False(packet.IsFreeze);
+
+ packet.IsFreeze = null;
+ Assert.Null(packet.IsFreeze);
+ }
+
+ [Fact]
+ public void AppType_SetToNull_ReturnsNull()
+ {
+ var packet = new Packet { AppType = 2 };
+ Assert.Equal(2, packet.AppType);
+
+ packet.AppType = null;
+ Assert.Null(packet.AppType);
+ }
+
+ [Fact]
+ public void Platform_SetToNull_ReturnsNull()
+ {
+ var packet = new Packet { Platform = 1 };
+ Assert.Equal(1, packet.Platform);
+
+ packet.Platform = null;
+ Assert.Null(packet.Platform);
+ }
+
+ [Fact]
+ public void ReleaseDate_Nullable_Works()
+ {
+ var packet = new Packet();
+ Assert.Null(packet.ReleaseDate);
+
+ var dt = DateTime.UtcNow;
+ packet.ReleaseDate = dt;
+ Assert.Equal(dt, packet.ReleaseDate);
+
+ packet.ReleaseDate = null;
+ Assert.Null(packet.ReleaseDate);
+ }
+}
diff --git a/tests/CoreTest/Configuration/UpdateOptionValueTests.cs b/tests/CoreTest/Configuration/UpdateOptionValueTests.cs
new file mode 100644
index 00000000..880e8661
--- /dev/null
+++ b/tests/CoreTest/Configuration/UpdateOptionValueTests.cs
@@ -0,0 +1,202 @@
+using GeneralUpdate.Core.Configuration;
+
+namespace CoreTest.Configuration;
+
+///
+/// AAAT unit tests for — the strongly-typed option value wrapper.
+/// Covers: construction, Option property, GetValue, ToString (normal/edge/null), value semantics.
+///
+public class UpdateOptionValueTests
+{
+ #region Construction & Basic Properties
+
+ [Fact]
+ public void Ctor_IntValue_StoresCorrectly()
+ {
+ var option = UpdateOption.ValueOf("INT_KEY_UV", 0);
+ var value = new UpdateOptionValue(option, 42);
+
+ Assert.Same(option, value.Option);
+ Assert.Equal(42, value.GetValue());
+ }
+
+ [Fact]
+ public void Ctor_StringValue_StoresCorrectly()
+ {
+ var option = UpdateOption.ValueOf("STR_KEY");
+ var value = new UpdateOptionValue(option, "hello");
+
+ Assert.Same(option, value.Option);
+ Assert.Equal("hello", value.GetValue());
+ }
+
+ [Fact]
+ public void Ctor_NullableBool_StoresCorrectly()
+ {
+ var option = UpdateOption.ValueOf("BOOL_KEY");
+ var value = new UpdateOptionValue(option, true);
+
+ Assert.Equal(true, value.GetValue());
+ }
+
+ [Fact]
+ public void Ctor_NullReferenceType_StoresCorrectly()
+ {
+ var option = UpdateOption.ValueOf("NULL_KEY");
+ var value = new UpdateOptionValue(option, null!);
+
+ Assert.Null(value.GetValue());
+ }
+
+ [Fact]
+ public void Ctor_DefaultValueType_StoresCorrectly()
+ {
+ var option = UpdateOption.ValueOf("DEFAULT_INT");
+ var value = new UpdateOptionValue(option, default);
+
+ Assert.Equal(0, value.GetValue());
+ }
+
+ #endregion
+
+ #region GetValue returns correct type
+
+ [Fact]
+ public void GetValue_Int_ReturnsBoxedInt()
+ {
+ var option = UpdateOption.ValueOf("BOXED_INT", 0);
+ var value = new UpdateOptionValue(option, 99);
+
+ var result = value.GetValue();
+
+ Assert.IsType(result);
+ Assert.Equal(99, result);
+ }
+
+ [Fact]
+ public void GetValue_BaseClass_ReturnsBoxedValue()
+ {
+ var option = UpdateOption.ValueOf("BASE_CLASS", 0);
+ var value = new UpdateOptionValue(option, 7);
+ UpdateOptionValue baseRef = value;
+
+ var result = baseRef.GetValue();
+
+ Assert.IsType(result);
+ Assert.Equal(7, result);
+ }
+
+ #endregion
+
+ #region ToString
+
+ [Fact]
+ public void ToString_NonNullValue_ReturnsValueToString()
+ {
+ var option = UpdateOption.ValueOf("TOSTR1", 0);
+ var value = new UpdateOptionValue(option, 42);
+
+ Assert.Equal("42", value.ToString());
+ }
+
+ [Fact]
+ public void ToString_NullValue_ReturnsEmptyString()
+ {
+ var option = UpdateOption.ValueOf("TOSTR2");
+ var value = new UpdateOptionValue(option, null!);
+
+ Assert.Equal(string.Empty, value.ToString());
+ }
+
+ [Fact]
+ public void ToString_EmptyStringValue_ReturnsEmptyString()
+ {
+ var option = UpdateOption.ValueOf("TOSTR3");
+ var value = new UpdateOptionValue(option, string.Empty);
+
+ Assert.Equal(string.Empty, value.ToString());
+ }
+
+ [Fact]
+ public void ToString_DateTimeValue_ReturnsFormattedDateTime()
+ {
+ var option = UpdateOption.ValueOf("TOSTR_DT");
+ var dt = new DateTime(2024, 6, 15, 0, 0, 0, DateTimeKind.Utc);
+ var value = new UpdateOptionValue(option, dt);
+
+ var result = value.ToString();
+
+ Assert.Contains("2024", result);
+ }
+
+ [Fact]
+ public void ToString_BooleanValue_ReturnsTrueOrFalse()
+ {
+ var option = UpdateOption.ValueOf("TOSTR_BOOL");
+ var trueVal = new UpdateOptionValue(option, true);
+ var falseVal = new UpdateOptionValue(option, false);
+
+ Assert.Equal("True", trueVal.ToString());
+ Assert.Equal("False", falseVal.ToString());
+ }
+
+ #endregion
+
+ #region Option property validation
+
+ [Fact]
+ public void Option_ReturnsTheSameOptionInstance()
+ {
+ var option1 = UpdateOption.ValueOf("OPT_CHECK_1", 1);
+ var option2 = UpdateOption.ValueOf("OPT_CHECK_2", 2);
+ var value1 = new UpdateOptionValue(option1, 100);
+ var value2 = new UpdateOptionValue(option2, 200);
+
+ Assert.Same(option1, value1.Option);
+ Assert.Same(option2, value2.Option);
+ Assert.NotSame(value1.Option, value2.Option);
+ }
+
+ #endregion
+
+ #region Edge cases
+
+ [Fact]
+ public void Ctor_MaxInt_StoresCorrectly()
+ {
+ var option = UpdateOption.ValueOf("MAX_INT", 0);
+ var value = new UpdateOptionValue(option, int.MaxValue);
+
+ Assert.Equal(int.MaxValue, value.GetValue());
+ }
+
+ [Fact]
+ public void Ctor_MinInt_StoresCorrectly()
+ {
+ var option = UpdateOption.ValueOf("MIN_INT", 0);
+ var value = new UpdateOptionValue(option, int.MinValue);
+
+ Assert.Equal(int.MinValue, value.GetValue());
+ }
+
+ [Fact]
+ public void Ctor_LongString_StoresCorrectly()
+ {
+ var option = UpdateOption.ValueOf("LONG_STR");
+ var longStr = new string('x', 10000);
+ var value = new UpdateOptionValue(option, longStr);
+
+ Assert.Equal(longStr, value.GetValue());
+ }
+
+ [Fact]
+ public void Ctor_DoubleNaN_StoresCorrectly()
+ {
+ var option = UpdateOption.ValueOf("NAN_KEY");
+ var value = new UpdateOptionValue(option, double.NaN);
+
+ Assert.True(double.IsNaN((double)value.GetValue()));
+ }
+
+ #endregion
+}
diff --git a/tests/CoreTest/Configuration/UpdateOptionsStaticTests.cs b/tests/CoreTest/Configuration/UpdateOptionsStaticTests.cs
new file mode 100644
index 00000000..c3fd3105
--- /dev/null
+++ b/tests/CoreTest/Configuration/UpdateOptionsStaticTests.cs
@@ -0,0 +1,135 @@
+using GeneralUpdate.Core.Configuration;
+
+namespace CoreTest.Configuration;
+
+///
+/// AAAT unit tests for — static option key definitions.
+/// Validates the default values as defined in the source.
+///
+public class UpdateOptionsStaticTests
+{
+ #region Static option key existence & defaults
+
+ [Fact]
+ public void AppType_HasCorrectDefault()
+ {
+ Assert.Equal(GeneralUpdate.Core.Configuration.AppType.Client, UpdateOptions.AppType.DefaultValue);
+ }
+
+ [Fact]
+ public void DiffMode_HasCorrectDefault()
+ {
+ Assert.Equal(DiffMode.Serial, UpdateOptions.DiffMode.DefaultValue);
+ }
+
+ [Fact]
+ public void Encoding_HasCorrectDefault()
+ {
+ Assert.Equal(System.Text.Encoding.UTF8, UpdateOptions.Encoding.DefaultValue);
+ }
+
+ [Fact]
+ public void Format_HasCorrectDefault()
+ {
+ Assert.Equal("ZIP", UpdateOptions.Format.DefaultValue);
+ }
+
+ [Fact]
+ public void DownloadTimeout_HasCorrectDefault()
+ {
+ Assert.Equal(30, UpdateOptions.DownloadTimeout.DefaultValue);
+ }
+
+ [Fact]
+ public void PatchEnabled_HasCorrectDefault()
+ {
+ Assert.True(UpdateOptions.PatchEnabled.DefaultValue);
+ }
+
+ [Fact]
+ public void BackupEnabled_HasCorrectDefault()
+ {
+ Assert.True(UpdateOptions.BackupEnabled.DefaultValue);
+ }
+
+ [Fact]
+ public void Silent_HasCorrectDefault()
+ {
+ Assert.False(UpdateOptions.Silent.DefaultValue);
+ }
+
+ [Fact]
+ public void SilentAutoInstall_HasCorrectDefault()
+ {
+ Assert.False(UpdateOptions.SilentAutoInstall.DefaultValue);
+ }
+
+ [Fact]
+ public void SilentPollIntervalMinutes_HasCorrectDefault()
+ {
+ Assert.Equal(60, UpdateOptions.SilentPollIntervalMinutes.DefaultValue);
+ }
+
+ [Fact]
+ public void MaxConcurrency_HasCorrectDefault()
+ {
+ Assert.Equal(3, UpdateOptions.MaxConcurrency.DefaultValue);
+ }
+
+ [Fact]
+ public void EnableResume_HasCorrectDefault()
+ {
+ Assert.True(UpdateOptions.EnableResume.DefaultValue);
+ }
+
+ [Fact]
+ public void RetryCount_HasCorrectDefault()
+ {
+ Assert.Equal(3, UpdateOptions.RetryCount.DefaultValue);
+ }
+
+ [Fact]
+ public void VerifyChecksum_HasCorrectDefault()
+ {
+ Assert.True(UpdateOptions.VerifyChecksum.DefaultValue);
+ }
+
+ [Fact]
+ public void RetryInterval_HasCorrectDefault()
+ {
+ Assert.Equal(TimeSpan.FromSeconds(1), UpdateOptions.RetryInterval.DefaultValue);
+ }
+
+ #endregion
+
+ #region Singleton identity
+
+ [Fact]
+ public void AppType_RepeatedAccess_ReturnsSameInstance()
+ {
+ var a = UpdateOptions.AppType;
+ var b = UpdateOptions.AppType;
+ Assert.Same(a, b);
+ }
+
+ [Fact]
+ public void AllOptions_RepeatedAccess_ReturnsSameInstance()
+ {
+ Assert.Same(UpdateOptions.DiffMode, UpdateOptions.DiffMode);
+ Assert.Same(UpdateOptions.Format, UpdateOptions.Format);
+ Assert.Same(UpdateOptions.MaxConcurrency, UpdateOptions.MaxConcurrency);
+ Assert.Same(UpdateOptions.RetryCount, UpdateOptions.RetryCount);
+ }
+
+ #endregion
+
+ #region Hub option
+
+ [Fact]
+ public void Hub_NotSet_HasNullDefault()
+ {
+ Assert.Null(UpdateOptions.Hub.DefaultValue);
+ }
+
+ #endregion
+}
diff --git a/tests/CoreTest/Configuration/VersionOSSTests.cs b/tests/CoreTest/Configuration/VersionOSSTests.cs
new file mode 100644
index 00000000..1f60e295
--- /dev/null
+++ b/tests/CoreTest/Configuration/VersionOSSTests.cs
@@ -0,0 +1,100 @@
+using GeneralUpdate.Core.Configuration;
+
+namespace CoreTest.Configuration;
+
+///
+/// AAAT unit tests for — property defaults and set/get.
+/// Covers: all properties, DateTime precision, null/empty strings.
+///
+public class VersionOSSTests
+{
+ [Fact]
+ public void Ctor_Default_AllPropertiesAreDefault()
+ {
+ var version = new VersionOSS();
+
+ Assert.Equal(default(DateTime), version.PubTime);
+ Assert.Null(version.PacketName);
+ Assert.Null(version.Hash);
+ Assert.Null(version.Version);
+ Assert.Null(version.Url);
+ }
+
+ [Fact]
+ public void FullAssignment_AllPropertiesSet()
+ {
+ var pubTime = new DateTime(2025, 1, 15, 10, 30, 0, DateTimeKind.Utc);
+
+ var version = new VersionOSS
+ {
+ PubTime = pubTime,
+ PacketName = "Release-2.0.0.zip",
+ Hash = "def456abc",
+ Version = "2.0.0",
+ Url = "https://oss.example.com/bucket/release.zip"
+ };
+
+ Assert.Equal(pubTime, version.PubTime);
+ Assert.Equal("Release-2.0.0.zip", version.PacketName);
+ Assert.Equal("def456abc", version.Hash);
+ Assert.Equal("2.0.0", version.Version);
+ Assert.Equal("https://oss.example.com/bucket/release.zip", version.Url);
+ }
+
+ [Fact]
+ public void PacketName_SetToNull_Works()
+ {
+ var version = new VersionOSS { PacketName = "something" };
+ Assert.Equal("something", version.PacketName);
+
+ version.PacketName = null;
+ Assert.Null(version.PacketName);
+ }
+
+ [Fact]
+ public void Hash_SetToNull_Works()
+ {
+ var version = new VersionOSS { Hash = "hash" };
+ version.Hash = null;
+ Assert.Null(version.Hash);
+ }
+
+ [Fact]
+ public void Version_SetToNull_Works()
+ {
+ var version = new VersionOSS { Version = "1.0" };
+ version.Version = null;
+ Assert.Null(version.Version);
+ }
+
+ [Fact]
+ public void Url_SetToNull_Works()
+ {
+ var version = new VersionOSS { Url = "https://a" };
+ version.Url = null;
+ Assert.Null(version.Url);
+ }
+
+ [Fact]
+ public void PubTime_UtcNow_StoredCorrectly()
+ {
+ var now = DateTime.UtcNow;
+ var version = new VersionOSS { PubTime = now };
+
+ Assert.Equal(now, version.PubTime);
+ }
+
+ [Fact]
+ public void PubTime_MinValue_StoredCorrectly()
+ {
+ var version = new VersionOSS { PubTime = DateTime.MinValue };
+ Assert.Equal(DateTime.MinValue, version.PubTime);
+ }
+
+ [Fact]
+ public void PubTime_MaxValue_StoredCorrectly()
+ {
+ var version = new VersionOSS { PubTime = DateTime.MaxValue };
+ Assert.Equal(DateTime.MaxValue, version.PubTime);
+ }
+}
diff --git a/tests/CoreTest/Configuration/VersionRespDTOTests.cs b/tests/CoreTest/Configuration/VersionRespDTOTests.cs
new file mode 100644
index 00000000..8eb06a79
--- /dev/null
+++ b/tests/CoreTest/Configuration/VersionRespDTOTests.cs
@@ -0,0 +1,138 @@
+using GeneralUpdate.Core.Configuration;
+
+namespace CoreTest.Configuration;
+
+///
+/// AAAT unit tests for and .
+/// Covers: default construction, property set/get, generic type resolution, null body.
+///
+public class VersionRespDTOTests
+{
+ [Fact]
+ public void Ctor_Default_CodeIsZero()
+ {
+ var resp = new VersionRespDTO();
+ Assert.Equal(0, resp.Code);
+ }
+
+ [Fact]
+ public void Ctor_Default_BodyIsNull()
+ {
+ var resp = new VersionRespDTO();
+ Assert.Null(resp.Body);
+ }
+
+ [Fact]
+ public void Ctor_Default_MessageIsNull()
+ {
+ var resp = new VersionRespDTO();
+ Assert.Null(resp.Message);
+ }
+
+ [Fact]
+ public void FullAssignment_AllPropertiesSet()
+ {
+ var versions = new List
+ {
+ new() { Version = "2.0.0", Hash = "abc", Name = "update.zip" }
+ };
+
+ var resp = new VersionRespDTO
+ {
+ Code = 200,
+ Body = versions,
+ Message = "success"
+ };
+
+ Assert.Equal(200, resp.Code);
+ Assert.Same(versions, resp.Body);
+ Assert.Equal("success", resp.Message);
+ Assert.Single(resp.Body);
+ Assert.Equal("2.0.0", resp.Body[0].Version);
+ }
+
+ [Fact]
+ public void BaseResponseDTO_WithStringBody_Works()
+ {
+ var resp = new BaseResponseDTO
+ {
+ Code = 404,
+ Body = "not found",
+ Message = "error"
+ };
+
+ Assert.Equal(404, resp.Code);
+ Assert.Equal("not found", resp.Body);
+ Assert.Equal("error", resp.Message);
+ }
+
+ [Fact]
+ public void BaseResponseDTO_WithIntBody_Works()
+ {
+ var resp = new BaseResponseDTO
+ {
+ Code = 200,
+ Body = 42,
+ Message = "ok"
+ };
+
+ Assert.Equal(200, resp.Code);
+ Assert.Equal(42, resp.Body);
+ Assert.Equal("ok", resp.Message);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(200)]
+ [InlineData(400)]
+ [InlineData(500)]
+ public void Code_SetVariousValues_Works(int code)
+ {
+ var resp = new VersionRespDTO { Code = code };
+ Assert.Equal(code, resp.Code);
+ }
+
+ [Fact]
+ public void Code_NegativeValue_Works()
+ {
+ var resp = new VersionRespDTO { Code = -1 };
+ Assert.Equal(-1, resp.Code);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("ok")]
+ [InlineData("error message with special chars: !@#$%^&*()")]
+ public void Message_SetVariousValues_Works(string message)
+ {
+ var resp = new VersionRespDTO { Message = message };
+ Assert.Equal(message, resp.Message);
+ }
+
+ [Fact]
+ public void Body_EmptyList_Works()
+ {
+ var resp = new VersionRespDTO
+ {
+ Code = 200,
+ Body = new List(),
+ Message = "no updates"
+ };
+
+ Assert.NotNull(resp.Body);
+ Assert.Empty(resp.Body);
+ }
+
+ [Fact]
+ public void Body_WithMultipleVersions_Works()
+ {
+ var versions = Enumerable.Range(1, 5).Select(i =>
+ new VersionInfo { Version = $"{i}.0.0", Hash = $"hash{i}" }).ToList();
+
+ var resp = new VersionRespDTO { Body = versions };
+
+ Assert.Equal(5, resp.Body.Count);
+ Assert.Equal("3.0.0", resp.Body[2].Version);
+ }
+}
diff --git a/tests/CoreTest/Download/DefaultDownloadPipelineTests.cs b/tests/CoreTest/Download/DefaultDownloadPipelineTests.cs
new file mode 100644
index 00000000..ca8f724a
--- /dev/null
+++ b/tests/CoreTest/Download/DefaultDownloadPipelineTests.cs
@@ -0,0 +1,199 @@
+using GeneralUpdate.Core.Download.Pipeline;
+
+namespace CoreTest.Download;
+
+///
+/// AAAT unit tests for — SHA256 hash verification pipeline.
+/// Covers: no-hash passthrough, matching hash, mismatched hash, null hash, empty hash, file not found, cancelled token, whitespace hash.
+///
+public class DefaultDownloadPipelineTests
+{
+ private static string TempFile(string content = "test content")
+ {
+ var path = Path.GetTempFileName();
+ File.WriteAllText(path, content);
+ return path;
+ }
+
+ #region No hash — passthrough
+
+ [Fact]
+ public async Task ProcessAsync_NullHash_ReturnsPath()
+ {
+ var pipeline = new DefaultDownloadPipeline(null);
+ var filePath = TempFile();
+
+ try
+ {
+ var result = await pipeline.ProcessAsync(filePath);
+ Assert.Equal(filePath, result);
+ }
+ finally { TryDelete(filePath); }
+ }
+
+ [Fact]
+ public async Task ProcessAsync_EmptyHash_ReturnsPath()
+ {
+ var pipeline = new DefaultDownloadPipeline(string.Empty);
+ var filePath = TempFile();
+
+ try
+ {
+ var result = await pipeline.ProcessAsync(filePath);
+ Assert.Equal(filePath, result);
+ }
+ finally { TryDelete(filePath); }
+ }
+
+ [Fact]
+ public async Task ProcessAsync_WhitespaceHash_TriggersVerification()
+ {
+ // Whitespace is NOT null or empty, so it triggers hash verification which will fail
+ var pipeline = new DefaultDownloadPipeline(" ");
+ var filePath = TempFile();
+
+ try
+ {
+ await Assert.ThrowsAsync(() => pipeline.ProcessAsync(filePath));
+ }
+ finally { TryDelete(filePath); }
+ }
+
+ [Fact]
+ public async Task ProcessAsync_DefaultCtor_NoHash_ReturnsPath()
+ {
+ var pipeline = new DefaultDownloadPipeline();
+ var filePath = TempFile();
+
+ try
+ {
+ var result = await pipeline.ProcessAsync(filePath);
+ Assert.Equal(filePath, result);
+ }
+ finally { TryDelete(filePath); }
+ }
+
+ #endregion
+
+ #region Hash verification
+
+ [Fact]
+ public async Task ProcessAsync_MatchingHash_Succeeds()
+ {
+ var filePath = TempFile("Hello World");
+ var expectedHash = ComputeSha256(filePath);
+
+ var pipeline = new DefaultDownloadPipeline(expectedHash);
+
+ try
+ {
+ var result = await pipeline.ProcessAsync(filePath);
+ Assert.Equal(filePath, result);
+ }
+ finally { TryDelete(filePath); }
+ }
+
+ [Fact]
+ public async Task ProcessAsync_MismatchedHash_ThrowsInvalidDataException()
+ {
+ var filePath = TempFile("Hello World");
+
+ // Deliberately wrong hash
+ var pipeline = new DefaultDownloadPipeline("0000000000000000000000000000000000000000000000000000000000000000");
+ try
+ {
+ await Assert.ThrowsAsync(() => pipeline.ProcessAsync(filePath));
+ }
+ finally { TryDelete(filePath); }
+ }
+
+ [Fact]
+ public async Task ProcessAsync_HashCaseInsensitive_MatchSucceeds()
+ {
+ var filePath = TempFile("case test");
+ var lowerHash = ComputeSha256(filePath);
+ var upperHash = lowerHash.ToUpperInvariant();
+
+ Assert.NotEqual(lowerHash, upperHash); // confirm case differs
+
+ var pipeline = new DefaultDownloadPipeline(upperHash);
+
+ try
+ {
+ var result = await pipeline.ProcessAsync(filePath);
+ Assert.Equal(filePath, result);
+ }
+ finally { TryDelete(filePath); }
+ }
+
+ #endregion
+
+ #region Error handling
+
+ [Fact]
+ public async Task ProcessAsync_FileNotFound_Throws()
+ {
+ var pipeline = new DefaultDownloadPipeline("abc");
+ var nonExistentPath = Path.Combine(Path.GetTempPath(), "nonexistent_" + Guid.NewGuid().ToString("N") + ".bin");
+ await Assert.ThrowsAnyAsync(() =>
+ pipeline.ProcessAsync(nonExistentPath));
+ }
+
+ [Fact]
+ public async Task ProcessAsync_CancelledTokenWithHash_ThrowsOperationCanceledException()
+ {
+ // When a hash IS provided, the pipeline actually performs async I/O (SHA256),
+ // which can observe cancellation
+ var filePath = TempFile("test content for cancellation");
+ var expectedHash = ComputeSha256(filePath);
+ var pipeline = new DefaultDownloadPipeline(expectedHash);
+ using var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ try
+ {
+ await Assert.ThrowsAnyAsync(() =>
+ pipeline.ProcessAsync(filePath, cts.Token));
+ }
+ finally { TryDelete(filePath); }
+ }
+
+ #endregion
+
+ #region Empty file
+
+ [Fact]
+ public async Task ProcessAsync_EmptyFileHashMatch_Succeeds()
+ {
+ var filePath = TempFile(string.Empty);
+ var expectedHash = ComputeSha256(filePath);
+
+ var pipeline = new DefaultDownloadPipeline(expectedHash);
+
+ try
+ {
+ var result = await pipeline.ProcessAsync(filePath);
+ Assert.Equal(filePath, result);
+ }
+ finally { TryDelete(filePath); }
+ }
+
+ #endregion
+
+ #region Helpers
+
+ private static string ComputeSha256(string path)
+ {
+ using var sha = System.Security.Cryptography.SHA256.Create();
+ using var fs = File.OpenRead(path);
+ var hash = sha.ComputeHash(fs);
+ return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
+ }
+
+ private static void TryDelete(string path)
+ {
+ try { if (File.Exists(path)) File.Delete(path); } catch { }
+ }
+
+ #endregion
+}
diff --git a/tests/CoreTest/Download/DownloadModelsTests.cs b/tests/CoreTest/Download/DownloadModelsTests.cs
new file mode 100644
index 00000000..76e6969b
--- /dev/null
+++ b/tests/CoreTest/Download/DownloadModelsTests.cs
@@ -0,0 +1,203 @@
+using GeneralUpdate.Core.Download.Models;
+
+namespace CoreTest.Download;
+
+///
+/// AAAT unit tests for , ,
+/// , record types.
+/// Covers: default construction, value equality, Empty plan, HasAssets, all statuses/priorities.
+///
+public class DownloadModelsTests
+{
+ #region DownloadAsset
+
+ [Fact]
+ public void DownloadAsset_Defaults_AreSensible()
+ {
+ var asset = new DownloadAsset("test", "http://url", 100, null, "1.0.0");
+
+ Assert.Equal("test", asset.Name);
+ Assert.Equal("http://url", asset.Url);
+ Assert.Equal(100, asset.Size);
+ Assert.Null(asset.SHA256);
+ Assert.Equal("1.0.0", asset.Version);
+ Assert.Equal(DownloadPriority.Normal, asset.Priority);
+ Assert.False(asset.IsCrossVersion);
+ Assert.Null(asset.FromVersion);
+ Assert.Null(asset.MinClientVersion);
+ Assert.False(asset.IsForcibly);
+ Assert.False(asset.IsFreeze);
+ }
+
+ [Fact]
+ public void DownloadAsset_FullySpecified_AllPropertiesSet()
+ {
+ var asset = new DownloadAsset(
+ "package.zip", "https://cdn/pkg.zip", 1024, "abc123", "3.0.0",
+ DownloadPriority.High, true, "1.0.0", "2.0.0",
+ "srcHash", "tgtHash", true, false
+ );
+
+ Assert.Equal("package.zip", asset.Name);
+ Assert.Equal("https://cdn/pkg.zip", asset.Url);
+ Assert.Equal(1024, asset.Size);
+ Assert.Equal("abc123", asset.SHA256);
+ Assert.Equal("3.0.0", asset.Version);
+ Assert.Equal(DownloadPriority.High, asset.Priority);
+ Assert.True(asset.IsCrossVersion);
+ Assert.Equal("1.0.0", asset.FromVersion);
+ Assert.Equal("2.0.0", asset.MinClientVersion);
+ Assert.Equal("srcHash", asset.SourceArchiveHash);
+ Assert.Equal("tgtHash", asset.TargetArchiveHash);
+ Assert.True(asset.IsForcibly);
+ Assert.False(asset.IsFreeze);
+ }
+
+ [Fact]
+ public void DownloadAsset_ValueEquality_SamePropsEqual()
+ {
+ var a = new DownloadAsset("a", "url", 100, "hash", "1.0", IsForcibly: true);
+ var b = new DownloadAsset("a", "url", 100, "hash", "1.0", IsForcibly: true);
+
+ Assert.Equal(a, b);
+ Assert.Equal(a.GetHashCode(), b.GetHashCode());
+ }
+
+ [Fact]
+ public void DownloadAsset_ValueEquality_DifferentForciblyNotEqual()
+ {
+ var a = new DownloadAsset("a", "url", 100, "hash", "1.0", IsForcibly: true);
+ var b = new DownloadAsset("a", "url", 100, "hash", "1.0", IsForcibly: false);
+
+ Assert.NotEqual(a, b);
+ }
+
+ #endregion
+
+ #region DownloadPlan
+
+ [Fact]
+ public void DownloadPlan_Empty_HasNoAssets()
+ {
+ var plan = DownloadPlan.Empty;
+ Assert.False(plan.HasAssets);
+ Assert.Empty(plan.Assets);
+ Assert.False(plan.IsForcibly);
+ }
+
+ [Fact]
+ public void DownloadPlan_Empty_IsSingleton()
+ {
+ var a = DownloadPlan.Empty;
+ var b = DownloadPlan.Empty;
+ Assert.Same(a, b);
+ }
+
+ [Fact]
+ public void DownloadPlan_WithAssets_HasAssetsTrue()
+ {
+ var assets = new List { new("a", "u", 100, null, "2.0") };
+ var plan = new DownloadPlan(assets, false);
+
+ Assert.True(plan.HasAssets);
+ Assert.Single(plan.Assets);
+ }
+
+ [Fact]
+ public void DownloadPlan_Empty_IsForciblyFalse()
+ {
+ var plan = DownloadPlan.Empty;
+ Assert.False(plan.IsForcibly);
+ }
+
+ [Fact]
+ public void DownloadPlan_ForciblyTrue_Stored()
+ {
+ var plan = new DownloadPlan(new List(), true);
+ Assert.True(plan.IsForcibly);
+ }
+
+ #endregion
+
+ #region DownloadProgress
+
+ [Fact]
+ public void DownloadProgress_AllPropertiesAssigned()
+ {
+ var dp = new DownloadProgress("asset.zip", 512, 1024, 50.0, DownloadStatus.Downloading);
+
+ Assert.Equal("asset.zip", dp.AssetName);
+ Assert.Equal(512, dp.BytesDownloaded);
+ Assert.Equal(1024, dp.TotalBytes);
+ Assert.Equal(50.0, dp.Percentage);
+ Assert.Equal(DownloadStatus.Downloading, dp.Status);
+ }
+
+ [Fact]
+ public void DownloadProgress_NullAssetName_Works()
+ {
+ var dp = new DownloadProgress(null, 100, 200, 50.0, DownloadStatus.Pending);
+ Assert.Null(dp.AssetName);
+ }
+
+ [Fact]
+ public void DownloadProgress_NullTotalBytes_Works()
+ {
+ var dp = new DownloadProgress("a.zip", 500, null, 0, DownloadStatus.Downloading);
+ Assert.Null(dp.TotalBytes);
+ }
+
+ [Fact]
+ public void DownloadProgress_AllStatuses_Supported()
+ {
+ foreach (DownloadStatus status in Enum.GetValues())
+ {
+ var dp = new DownloadProgress("a", 0, 0, 0, status);
+ Assert.Equal(status, dp.Status);
+ }
+ }
+
+ #endregion
+
+ #region DownloadResult
+
+ [Fact]
+ public void DownloadResult_Success_AllFieldsSet()
+ {
+ var asset = new DownloadAsset("pkg.zip", "http://u", 500, "hash", "2.0");
+ var result = new DownloadResult(asset, "/local/pkg.zip", 500,
+ TimeSpan.FromSeconds(5), 1, true, null);
+
+ Assert.Same(asset, result.Asset);
+ Assert.Equal("/local/pkg.zip", result.LocalPath);
+ Assert.Equal(500, result.DownloadedBytes);
+ Assert.Equal(TimeSpan.FromSeconds(5), result.Duration);
+ Assert.Equal(1, result.RetryCount);
+ Assert.True(result.Success);
+ Assert.Null(result.ErrorMessage);
+ }
+
+ [Fact]
+ public void DownloadResult_Failure_HasErrorMessage()
+ {
+ var asset = new DownloadAsset("fail.zip", "http://u", 100, null, "1.0");
+ var result = new DownloadResult(asset, "", 0, TimeSpan.Zero, 3, false, "Network timeout");
+
+ Assert.False(result.Success);
+ Assert.Equal(3, result.RetryCount);
+ Assert.Equal("Network timeout", result.ErrorMessage);
+ }
+
+ #endregion
+
+ #region DownloadPriority
+
+ [Fact]
+ public void DownloadPriority_Ordering_LowLessThanNormal()
+ {
+ Assert.True(DownloadPriority.Low < DownloadPriority.Normal);
+ Assert.True(DownloadPriority.Normal < DownloadPriority.High);
+ }
+
+ #endregion
+}
diff --git a/tests/CoreTest/Download/DownloadProgressReporterTests.cs b/tests/CoreTest/Download/DownloadProgressReporterTests.cs
new file mode 100644
index 00000000..0c44cda8
--- /dev/null
+++ b/tests/CoreTest/Download/DownloadProgressReporterTests.cs
@@ -0,0 +1,273 @@
+using GeneralUpdate.Core.Download;
+using GeneralUpdate.Core.Download.Models;
+using GeneralUpdate.Core.Download.Progress;
+using GeneralUpdate.Core.Event;
+
+namespace CoreTest.Download;
+
+[Collection("NonParallel_EventManager")]
+public class DownloadProgressReporterTests : IDisposable
+{
+ public void Dispose()
+ {
+ EventManager.Instance.Clear();
+ }
+
+ #region Progress callback
+
+ [Fact]
+ public void Report_InvokesOnProgressCallback()
+ {
+ DownloadProgress? captured = null;
+ var reporter = new DownloadProgressReporter(onProgress: p => captured = p);
+
+ var progress = new DownloadProgress("asset.zip", 500, 1000, 50.0, DownloadStatus.Downloading);
+ reporter.Report(progress);
+
+ Assert.NotNull(captured);
+ Assert.Equal("asset.zip", captured!.AssetName);
+ Assert.Equal(500, captured.BytesDownloaded);
+ Assert.Equal(1000, captured.TotalBytes);
+ Assert.Equal(50.0, captured.Percentage);
+ Assert.Equal(DownloadStatus.Downloading, captured.Status);
+ }
+
+ [Fact]
+ public void Report_NullProgressCallback_DoesNotThrow()
+ {
+ var reporter = new DownloadProgressReporter(onProgress: null);
+
+ var progress = new DownloadProgress("a.zip", 0, 100, 0, DownloadStatus.Pending);
+ var ex = Record.Exception(() => reporter.Report(progress));
+
+ Assert.Null(ex);
+ }
+
+ #endregion
+
+ #region Completed callback
+
+ [Fact]
+ public void Report_CompletedStatus_InvokesOnCompleted()
+ {
+ var completedInvoked = false;
+ var reporter = new DownloadProgressReporter(onProgress: null, onCompleted: () => completedInvoked = true);
+
+ var progress = new DownloadProgress("done.zip", 1000, 1000, 100.0, DownloadStatus.Completed);
+ reporter.Report(progress);
+
+ Assert.True(completedInvoked);
+ }
+
+ [Fact]
+ public void Report_NonCompletedStatus_DoesNotInvokeOnCompleted()
+ {
+ var completedInvoked = false;
+ var reporter = new DownloadProgressReporter(onProgress: null, onCompleted: () => completedInvoked = true);
+
+ var progress = new DownloadProgress("notdone.zip", 500, 1000, 50.0, DownloadStatus.Downloading);
+ reporter.Report(progress);
+
+ Assert.False(completedInvoked);
+ }
+
+ [Fact]
+ public void Report_CompletedStatus_NullOnCompleted_DoesNotThrow()
+ {
+ var reporter = new DownloadProgressReporter(onProgress: null, onCompleted: null);
+
+ var progress = new DownloadProgress("done.zip", 1000, 1000, 100.0, DownloadStatus.Completed);
+ var ex = Record.Exception(() => reporter.Report(progress));
+
+ Assert.Null(ex);
+ }
+
+ [Fact]
+ public void Report_FailedStatus_DoesNotInvokeOnCompleted()
+ {
+ var completedInvoked = false;
+ var reporter = new DownloadProgressReporter(onProgress: null, onCompleted: () => completedInvoked = true);
+
+ var progress = new DownloadProgress("fail.zip", 0, 100, 0, DownloadStatus.Failed);
+ reporter.Report(progress);
+
+ Assert.False(completedInvoked);
+ }
+
+ #endregion
+
+ #region Event dispatch on Completed
+
+ [Fact]
+ public void Report_CompletedStatus_DispatchesCompletedEvent()
+ {
+ MultiDownloadCompletedEventArgs? captured = null;
+ Action