diff --git a/tests/CoreTest/Configuration/ObjectTranslatorTests.cs b/tests/CoreTest/Configuration/ObjectTranslatorTests.cs
new file mode 100644
index 00000000..fdb9a622
--- /dev/null
+++ b/tests/CoreTest/Configuration/ObjectTranslatorTests.cs
@@ -0,0 +1,114 @@
+using GeneralUpdate.Core;
+using GeneralUpdate.Core.Configuration;
+
+namespace CoreTest.Configuration;
+
+///
+/// Unit tests for following AAAT (Arrange-Act-Assert-TearDown).
+/// Implements to restore global state
+/// after each test, preventing cross-test leakage.
+///
+public class ObjectTranslatorTests : IDisposable
+{
+ private readonly bool _originalTracingEnabled;
+
+ public ObjectTranslatorTests()
+ {
+ // Capture original tracing state before any test modifies it
+ _originalTracingEnabled = GeneralTracer.IsTracingEnabled();
+ }
+
+ /// TearDown: restore tracing state to original value for test isolation.
+ public void Dispose()
+ {
+ GeneralTracer.SetTracingEnabled(_originalTracingEnabled);
+ GC.SuppressFinalize(this);
+ }
+
+ #region GetPacketHash
+
+ [Fact]
+ public void GetPacketHash_ValidVersionInfo_ReturnsFormattedHash()
+ {
+ // Arrange
+ GeneralTracer.SetTracingEnabled(true);
+ var version = new VersionInfo { Hash = "abc123def" };
+
+ // Act
+ var result = ObjectTranslator.GetPacketHash(version);
+
+ // Assert
+ Assert.Equal("[PacketHash]:abc123def ", result);
+ }
+
+ [Fact]
+ public void GetPacketHash_NonVersionInfoObject_ReturnsEmpty()
+ {
+ // Arrange
+ GeneralTracer.SetTracingEnabled(true);
+ var notVersion = "just a string";
+
+ // Act
+ var result = ObjectTranslator.GetPacketHash(notVersion);
+
+ // Assert
+ Assert.Equal(string.Empty, result);
+ }
+
+ [Fact]
+ public void GetPacketHash_TracingDisabled_ReturnsEmptyEvenForVersionInfo()
+ {
+ // Arrange
+ GeneralTracer.SetTracingEnabled(false);
+ var version = new VersionInfo { Hash = "abc123def" };
+
+ // Act
+ var result = ObjectTranslator.GetPacketHash(version);
+
+ // Assert
+ Assert.Equal(string.Empty, result);
+ }
+
+ [Fact]
+ public void GetPacketHash_NullObject_ReturnsEmpty()
+ {
+ // Arrange
+ GeneralTracer.SetTracingEnabled(true);
+
+ // Act
+ var result = ObjectTranslator.GetPacketHash(null!);
+
+ // Assert
+ Assert.Equal(string.Empty, result);
+ }
+
+ [Fact]
+ public void GetPacketHash_VersionInfoWithNullHash_ReturnsEmptyHashSegment()
+ {
+ // Arrange
+ GeneralTracer.SetTracingEnabled(true);
+ var version = new VersionInfo { Hash = null };
+
+ // Act
+ var result = ObjectTranslator.GetPacketHash(version);
+
+ // Assert
+ Assert.Equal("[PacketHash]: ", result);
+ }
+
+ [Fact]
+ public void GetPacketHash_VersionInfoWithEmptyHash_ReturnsEmptyHashSegment()
+ {
+ // Arrange
+ GeneralTracer.SetTracingEnabled(true);
+ var version = new VersionInfo { Hash = string.Empty };
+
+ // Act
+ var result = ObjectTranslator.GetPacketHash(version);
+
+ // Assert
+ Assert.Equal("[PacketHash]: ", result);
+ }
+
+ #endregion
+}
diff --git a/tests/CoreTest/Configuration/ProcessInfoTests.cs b/tests/CoreTest/Configuration/ProcessInfoTests.cs
index c2d78698..a10b3bd2 100644
--- a/tests/CoreTest/Configuration/ProcessInfoTests.cs
+++ b/tests/CoreTest/Configuration/ProcessInfoTests.cs
@@ -144,9 +144,128 @@ public void Ctor_AllParametersValid_AllPropertiesSet()
[Fact]
public void Ctor_EncodingUTF8_CompressEncodingWebNameIsUtf8()
{
+ // Arrange & Act
var info = new ProcessInfo("app", ExistingDir, "1.0", "2.0", null,
Encoding.UTF8, "ZIP", 30, "key",
SingleVersion, "url", "backup", null, null, null, null, null, null, null);
+
+ // Assert
Assert.Equal("utf-8", info.CompressEncoding);
}
+
+ [Fact]
+ public void Ctor_EncodingASCII_CompressEncodingWebNameIsAscii()
+ {
+ // Arrange & Act
+ var info = new ProcessInfo("app", ExistingDir, "1.0", "2.0", null,
+ Encoding.ASCII, "ZIP", 30, "key",
+ SingleVersion, "url", "backup", null, null, null, null, null, null, null);
+
+ // Assert
+ Assert.Equal("us-ascii", info.CompressEncoding);
+ }
+
+ [Fact]
+ public void Ctor_EncodingUnicode_CompressEncodingWebNameIsUtf16()
+ {
+ // Arrange & Act
+ var info = new ProcessInfo("app", ExistingDir, "1.0", "2.0", null,
+ Encoding.Unicode, "ZIP", 30, "key",
+ SingleVersion, "url", "backup", null, null, null, null, null, null, null);
+
+ // Assert
+ Assert.Equal("utf-16", info.CompressEncoding);
+ }
+
+ [Fact]
+ public void Ctor_NullableOptionalParams_AllowedAsNull()
+ {
+ // Arrange & Act — bowl, scheme, token, driverDirectory, blackList params are all nullable
+ var info = new ProcessInfo("app", ExistingDir, "1.0", "2.0", null,
+ Encoding.UTF8, "ZIP", 30, "key",
+ SingleVersion, "url", "backup",
+ null, null, null, null, null, null, null);
+
+ // Assert
+ Assert.Null(info.Bowl);
+ Assert.Null(info.Scheme);
+ Assert.Null(info.Token);
+ Assert.Null(info.DriverDirectory);
+ Assert.Null(info.BlackFileFormats);
+ Assert.Null(info.BlackFiles);
+ Assert.Null(info.SkipDirectorys);
+ }
+
+ [Fact]
+ public void Ctor_DefaultConstructor_AllPropertiesDefault()
+ {
+ // Arrange & Act
+ var info = new ProcessInfo();
+
+ // Assert — default constructor should produce empty/null state
+ Assert.Null(info.AppName);
+ Assert.Null(info.InstallPath);
+ Assert.Null(info.CurrentVersion);
+ Assert.Null(info.LastVersion);
+ Assert.Equal(0, info.DownloadTimeOut);
+ }
+
+ [Fact]
+ public void Ctor_MultipleVersions_AllStored()
+ {
+ // Arrange
+ var versions = new List
+ {
+ new() { Version = "1.0.0" },
+ new() { Version = "1.1.0" },
+ new() { Version = "2.0.0" }
+ };
+
+ // Act
+ var info = new ProcessInfo("app", ExistingDir, "1.0", "2.0", null,
+ Encoding.UTF8, "ZIP", 30, "key",
+ versions, "url", "backup", null, null, null, null, null, null, null);
+
+ // Assert
+ Assert.Equal(3, info.UpdateVersions.Count);
+ Assert.Equal("1.0.0", info.UpdateVersions[0].Version);
+ Assert.Equal("1.1.0", info.UpdateVersions[1].Version);
+ Assert.Equal("2.0.0", info.UpdateVersions[2].Version);
+ }
+
+ [Fact]
+ public void Ctor_UpdateLogUrlNull_Allowed()
+ {
+ // Arrange & Act
+ var info = new ProcessInfo("app", ExistingDir, "1.0", "2.0", null,
+ Encoding.UTF8, "ZIP", 30, "key",
+ SingleVersion, "url", "backup", null, null, null, null, null, null, null);
+
+ // Assert — UpdateLogUrl is explicitly allowed to be null
+ Assert.Null(info.UpdateLogUrl);
+ }
+
+ [Fact]
+ public void Ctor_AllBlacklistParamsPopulated_PreservedInOrder()
+ {
+ // Arrange
+ var formats = new List { ".log", ".tmp", ".cache" };
+ var files = new List { "secret.key", "config.ini" };
+ var dirs = new List { "logs", "temp", "backups" };
+
+ // Act
+ var info = new ProcessInfo("app", ExistingDir, "1.0", "2.0", null,
+ Encoding.UTF8, "ZIP", 30, "key",
+ SingleVersion, "url", "backup", null, null, null, null,
+ formats, files, dirs);
+
+ // Assert
+ Assert.Equal(3, info.BlackFileFormats!.Count);
+ Assert.Contains(".log", info.BlackFileFormats);
+ Assert.Equal(2, info.BlackFiles!.Count);
+ Assert.Contains("secret.key", info.BlackFiles);
+ Assert.Equal(3, info.SkipDirectorys!.Count);
+ Assert.Contains("logs", info.SkipDirectorys);
+ }
+
}
diff --git a/tests/CoreTest/Configuration/VersionModelTests.cs b/tests/CoreTest/Configuration/VersionModelTests.cs
new file mode 100644
index 00000000..576dfe5f
--- /dev/null
+++ b/tests/CoreTest/Configuration/VersionModelTests.cs
@@ -0,0 +1,382 @@
+using System.Text.Json;
+using GeneralUpdate.Core.Configuration;
+
+namespace CoreTest.Configuration;
+
+///
+/// Unit tests for configuration model/DTO classes following AAAT (Arrange-Act-Assert-TearDown).
+/// Covers: VersionInfo, VersionOSS, BaseResponseDTO, VersionRespDTO.
+///
+public class VersionModelTests
+{
+ #region VersionInfo — property defaults and JSON serialization
+
+ [Fact]
+ public void VersionInfo_DefaultInstance_AllNullablePropertiesAreNull()
+ {
+ // Arrange & Act
+ var vi = new VersionInfo();
+
+ // Assert
+ Assert.Equal(0, vi.RecordId);
+ Assert.Null(vi.Name);
+ Assert.Null(vi.Hash);
+ Assert.Null(vi.ReleaseDate);
+ Assert.Null(vi.Url);
+ Assert.Null(vi.Version);
+ Assert.Null(vi.AppType);
+ Assert.Null(vi.Platform);
+ Assert.Null(vi.ProductId);
+ Assert.Null(vi.IsForcibly);
+ Assert.Null(vi.Format);
+ Assert.Null(vi.Size);
+ Assert.Null(vi.AuthScheme);
+ Assert.Null(vi.AuthToken);
+ Assert.Null(vi.UpdateLog);
+ }
+
+ [Fact]
+ public void VersionInfo_AllProperties_SetCorrectly()
+ {
+ // Arrange
+ var releaseDate = new DateTime(2025, 6, 15, 10, 30, 0, DateTimeKind.Utc);
+
+ // Act
+ var vi = new VersionInfo
+ {
+ RecordId = 42,
+ Name = "update-package-v2.0.0",
+ Hash = "sha256:abcdef1234567890",
+ ReleaseDate = releaseDate,
+ Url = "https://cdn.example.com/packages/v2.0.0.zip",
+ Version = "2.0.0",
+ AppType = 1,
+ Platform = 2,
+ ProductId = "prod-001",
+ IsForcibly = true,
+ Format = ".zip",
+ Size = 104857600,
+ AuthScheme = "Bearer",
+ AuthToken = "token-xyz",
+ UpdateLog = "Bug fixes and performance improvements"
+ };
+
+ // Assert
+ Assert.Equal(42, vi.RecordId);
+ Assert.Equal("update-package-v2.0.0", vi.Name);
+ Assert.Equal("sha256:abcdef1234567890", vi.Hash);
+ Assert.Equal(releaseDate, vi.ReleaseDate);
+ Assert.Equal("https://cdn.example.com/packages/v2.0.0.zip", vi.Url);
+ Assert.Equal("2.0.0", vi.Version);
+ Assert.Equal(1, vi.AppType);
+ Assert.Equal(2, vi.Platform);
+ Assert.Equal("prod-001", vi.ProductId);
+ Assert.True(vi.IsForcibly);
+ Assert.Equal(".zip", vi.Format);
+ Assert.Equal(104857600, vi.Size);
+ Assert.Equal("Bearer", vi.AuthScheme);
+ Assert.Equal("token-xyz", vi.AuthToken);
+ Assert.Equal("Bug fixes and performance improvements", vi.UpdateLog);
+ }
+
+ [Fact]
+ public void VersionInfo_JsonRoundTrip_AllProperties()
+ {
+ // Arrange
+ var original = new VersionInfo
+ {
+ RecordId = 7,
+ Name = "MyPackage",
+ Hash = "abc123",
+ ReleaseDate = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc),
+ Url = "https://example.com/pkg",
+ Version = "1.0.0",
+ AppType = 1,
+ Platform = 0,
+ ProductId = "p1",
+ IsForcibly = false,
+ Format = ".zip",
+ Size = 5000000,
+ AuthScheme = "Bearer",
+ AuthToken = "tok",
+ UpdateLog = "v1 release"
+ };
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+
+ // Act
+ var json = JsonSerializer.Serialize(original);
+ var deserialized = JsonSerializer.Deserialize(json, options);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal(original.RecordId, deserialized.RecordId);
+ Assert.Equal(original.Name, deserialized.Name);
+ Assert.Equal(original.Hash, deserialized.Hash);
+ Assert.Equal(original.Url, deserialized.Url);
+ Assert.Equal(original.Version, deserialized.Version);
+ Assert.Equal(original.AppType, deserialized.AppType);
+ Assert.Equal(original.Platform, deserialized.Platform);
+ Assert.Equal(original.ProductId, deserialized.ProductId);
+ Assert.Equal(original.IsForcibly, deserialized.IsForcibly);
+ Assert.Equal(original.Format, deserialized.Format);
+ Assert.Equal(original.Size, deserialized.Size);
+ Assert.Equal(original.AuthScheme, deserialized.AuthScheme);
+ Assert.Equal(original.AuthToken, deserialized.AuthToken);
+ Assert.Equal(original.UpdateLog, deserialized.UpdateLog);
+ }
+
+ [Fact]
+ public void VersionInfo_JsonSerialization_UsesCorrectJsonPropertyNames()
+ {
+ // Arrange
+ var vi = new VersionInfo
+ {
+ RecordId = 1,
+ Name = "test",
+ Hash = "h",
+ ReleaseDate = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc),
+ Url = "http://x",
+ Version = "v1",
+ AppType = 1,
+ Platform = 2,
+ ProductId = "p",
+ IsForcibly = true,
+ Format = ".zip",
+ Size = 100,
+ AuthScheme = "Basic",
+ AuthToken = "t",
+ UpdateLog = "log"
+ };
+
+ // Act
+ var json = JsonSerializer.Serialize(vi);
+
+ // Assert — verify JSON property name casing matches [JsonPropertyName] attributes
+ Assert.Contains("\"recordId\"", json);
+ Assert.Contains("\"name\"", json);
+ Assert.Contains("\"hash\"", json);
+ Assert.Contains("\"releaseDate\"", json);
+ Assert.Contains("\"url\"", json);
+ Assert.Contains("\"version\"", json);
+ Assert.Contains("\"appType\"", json);
+ Assert.Contains("\"platform\"", json);
+ Assert.Contains("\"productId\"", json);
+ Assert.Contains("\"isForcibly\"", json);
+ Assert.Contains("\"format\"", json);
+ Assert.Contains("\"size\"", json);
+ Assert.Contains("\"authScheme\"", json);
+ Assert.Contains("\"authToken\"", json);
+ Assert.Contains("\"updateLog\"", json);
+ }
+
+ #endregion
+
+ #region VersionOSS — property defaults
+
+ [Fact]
+ public void VersionOSS_DefaultInstance_HasDefaultDate()
+ {
+ // Arrange & Act
+ var voss = new VersionOSS();
+
+ // Assert
+ Assert.Equal(default(DateTime), voss.PubTime);
+ Assert.Null(voss.PacketName);
+ Assert.Null(voss.Hash);
+ Assert.Null(voss.Version);
+ Assert.Null(voss.Url);
+ }
+
+ [Fact]
+ public void VersionOSS_AllProperties_SetCorrectly()
+ {
+ // Arrange
+ var pubTime = new DateTime(2025, 3, 1);
+
+ // Act
+ var voss = new VersionOSS
+ {
+ PubTime = pubTime,
+ PacketName = "update.zip",
+ Hash = "sha256:xyz",
+ Version = "2.0.0",
+ Url = "https://cdn.example.com/update.zip"
+ };
+
+ // Assert
+ Assert.Equal(pubTime, voss.PubTime);
+ Assert.Equal("update.zip", voss.PacketName);
+ Assert.Equal("sha256:xyz", voss.Hash);
+ Assert.Equal("2.0.0", voss.Version);
+ Assert.Equal("https://cdn.example.com/update.zip", voss.Url);
+ }
+
+ [Fact]
+ public void VersionOSS_JsonSerialization_UsesCorrectJsonPropertyNames()
+ {
+ // Arrange
+ var voss = new VersionOSS
+ {
+ PubTime = new DateTime(2025, 1, 1),
+ PacketName = "pkg.zip",
+ Hash = "abc",
+ Version = "1.0",
+ Url = "http://x"
+ };
+
+ // Act
+ var json = JsonSerializer.Serialize(voss);
+
+ // Assert
+ Assert.Contains("\"PubTime\"", json);
+ Assert.Contains("\"PacketName\"", json);
+ Assert.Contains("\"Hash\"", json);
+ Assert.Contains("\"Version\"", json);
+ Assert.Contains("\"Url\"", json);
+ }
+
+ [Fact]
+ public void VersionOSS_JsonRoundTrip_PreservesAllValues()
+ {
+ // Arrange
+ var pubTime = new DateTime(2025, 6, 1, 12, 0, 0);
+ var original = new VersionOSS
+ {
+ PubTime = pubTime,
+ PacketName = "package-v3.zip",
+ Hash = "sha256:deadbeef",
+ Version = "3.0.0",
+ Url = "https://cdn.example.com/v3.zip"
+ };
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+
+ // Act
+ var json = JsonSerializer.Serialize(original);
+ var deserialized = JsonSerializer.Deserialize(json, options);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal(original.PubTime, deserialized.PubTime);
+ Assert.Equal(original.PacketName, deserialized.PacketName);
+ Assert.Equal(original.Hash, deserialized.Hash);
+ Assert.Equal(original.Version, deserialized.Version);
+ Assert.Equal(original.Url, deserialized.Url);
+ }
+
+ #endregion
+
+ #region BaseResponseDTO — generic wrapper
+
+ [Fact]
+ public void BaseResponseDTO_IntBody_WrapsCorrectly()
+ {
+ // Arrange
+ var dto = new BaseResponseDTO
+ {
+ Code = 200,
+ Body = 42,
+ Message = "OK"
+ };
+
+ // Assert
+ Assert.Equal(200, dto.Code);
+ Assert.Equal(42, dto.Body);
+ Assert.Equal("OK", dto.Message);
+ }
+
+ [Fact]
+ public void BaseResponseDTO_StringBody_WrapsCorrectly()
+ {
+ // Arrange
+ var dto = new BaseResponseDTO
+ {
+ Code = 500,
+ Body = "Internal Server Error",
+ Message = "Something went wrong"
+ };
+
+ // Assert
+ Assert.Equal(500, dto.Code);
+ Assert.Equal("Internal Server Error", dto.Body);
+ Assert.Equal("Something went wrong", dto.Message);
+ }
+
+ [Fact]
+ public void BaseResponseDTO_VersionInfoList_WrapsCorrectly()
+ {
+ // Arrange
+ var versions = new List
+ {
+ new() { Version = "1.0.0", Hash = "abc" },
+ new() { Version = "2.0.0", Hash = "def" }
+ };
+ var dto = new BaseResponseDTO>
+ {
+ Code = 200,
+ Body = versions,
+ Message = "Success"
+ };
+
+ // Assert
+ Assert.Equal(200, dto.Code);
+ Assert.Equal(2, dto.Body.Count);
+ Assert.Equal("1.0.0", dto.Body[0].Version);
+ Assert.Equal("2.0.0", dto.Body[1].Version);
+ }
+
+ [Fact]
+ public void BaseResponseDTO_JsonSerialization_UsesCamelCasePropertyNames()
+ {
+ // Arrange
+ var dto = new BaseResponseDTO
+ {
+ Code = 200,
+ Body = "test-body",
+ Message = "success"
+ };
+
+ // Act
+ var json = JsonSerializer.Serialize(dto);
+
+ // Assert — property names use camelCase from [JsonPropertyName]
+ Assert.Contains("\"code\"", json);
+ Assert.Contains("\"body\"", json);
+ Assert.Contains("\"message\"", json);
+ }
+
+ [Fact]
+ public void BaseResponseDTO_DefaultInstance_CodeZero()
+ {
+ // Arrange & Act
+ var dto = new BaseResponseDTO();
+
+ // Assert
+ Assert.Equal(0, dto.Code);
+ Assert.Null(dto.Body);
+ Assert.Null(dto.Message);
+ }
+
+ #endregion
+
+ #region VersionRespDTO — typed alias
+
+ [Fact]
+ public void VersionRespDTO_IsAssignableToBaseResponse()
+ {
+ // Arrange
+ var dto = new VersionRespDTO
+ {
+ Code = 200,
+ Body = new List { new() { Version = "1.0.0" } },
+ Message = "OK"
+ };
+
+ // Assert
+ Assert.IsAssignableFrom>>(dto);
+ Assert.Equal(200, dto.Code);
+ Assert.Single(dto.Body);
+ Assert.Equal("1.0.0", dto.Body[0].Version);
+ }
+
+ #endregion
+}
diff --git a/tests/CoreTest/Event/EventListenerBatchTests.cs b/tests/CoreTest/Event/EventListenerBatchTests.cs
index b5dd2748..ad593570 100644
--- a/tests/CoreTest/Event/EventListenerBatchTests.cs
+++ b/tests/CoreTest/Event/EventListenerBatchTests.cs
@@ -5,12 +5,26 @@
namespace CoreTest.Event;
-public class EventListenerBatchTests
+///
+/// Batch tests for and
+/// following AAAT (Arrange-Act-Assert-TearDown).
+///
+public class EventListenerBatchTests : IDisposable
{
+ /// TearDown: clear singleton state after each test for isolation.
+ public void Dispose()
+ {
+ EventManager.Instance.Clear();
+ GC.SuppressFinalize(this);
+ }
+
[Fact]
public void IUpdateEventListener_AllMethodsDefined()
{
+ // Arrange & Act
var listener = new TestListener();
+
+ // Assert
Assert.NotNull(listener);
Assert.IsAssignableFrom(listener);
}
@@ -18,24 +32,36 @@ public void IUpdateEventListener_AllMethodsDefined()
[Fact]
public void UpdateEventListenerBase_AllDefaultNoOp()
{
+ // Arrange
var listener = new TestBaseListener();
var progress = new DownloadProgress("test.zip", 500, 1000, 50.0, DownloadStatus.Downloading);
- listener.OnUpdateInfo(new UpdateInfoEventArgs());
- listener.OnDownloadCompleted(new MultiDownloadCompletedEventArgs("1.0.0", true));
- listener.OnAllDownloadCompleted(new MultiAllDownloadCompletedEventArgs(true, new List<(object, string)>()));
- listener.OnDownloadError(new MultiDownloadErrorEventArgs(new System.Exception("e"), "1.0.0"));
- listener.OnDownloadStatistics(new MultiDownloadStatisticsEventArgs("1.0.0", TimeSpan.Zero, "0 B/s", 1000, 500, 50.0));
- listener.OnProgress(new ProgressEventArgs(progress));
- listener.OnException(new ExceptionEventArgs(new System.Exception("test"), "test"));
+ // Act — call all methods; base does nothing, so Record.Exception captures any throw
+ var ex = Record.Exception(() =>
+ {
+ listener.OnUpdateInfo(new UpdateInfoEventArgs());
+ listener.OnDownloadCompleted(new MultiDownloadCompletedEventArgs("1.0.0", true));
+ listener.OnAllDownloadCompleted(new MultiAllDownloadCompletedEventArgs(true, new List<(object, string)>()));
+ listener.OnDownloadError(new MultiDownloadErrorEventArgs(new System.Exception("e"), "1.0.0"));
+ listener.OnDownloadStatistics(new MultiDownloadStatisticsEventArgs("1.0.0", TimeSpan.Zero, "0 B/s", 1000, 500, 50.0));
+ listener.OnProgress(new ProgressEventArgs(progress));
+ listener.OnException(new ExceptionEventArgs(new System.Exception("test"), "test"));
+ });
+
+ // Assert
+ Assert.Null(ex);
}
[Fact]
public void ProgressEventArgs_WrapsDownloadProgress()
{
+ // Arrange
var progress = new DownloadProgress("test.zip", 500, 1000, 50.0, DownloadStatus.Downloading);
+
+ // Act
var args = new ProgressEventArgs(progress);
+ // Assert
Assert.Same(progress, args.Progress);
Assert.Equal("test.zip", args.Progress.AssetName);
Assert.Equal(500, args.Progress.BytesDownloaded);
@@ -45,9 +71,13 @@ public void ProgressEventArgs_WrapsDownloadProgress()
[Fact]
public void ExceptionEventArgs_HoldsException()
{
+ // Arrange
var ex = new System.InvalidOperationException("test error");
+
+ // Act
var args = new ExceptionEventArgs(ex, "Context message");
+ // Assert
Assert.Same(ex, args.Exception);
Assert.Equal("Context message", args.Message);
}
@@ -55,10 +85,12 @@ public void ExceptionEventArgs_HoldsException()
[Fact]
public void EventManager_ConcurrentSubscribeUnsubscribe()
{
+ // Arrange
var manager = EventManager.Instance;
int callCount = 0;
void Handler(object? s, System.EventArgs e) => System.Threading.Interlocked.Increment(ref callCount);
+ // Act
var tasks = new Task[10];
for (int i = 0; i < tasks.Length; i++)
{
@@ -71,13 +103,15 @@ public void EventManager_ConcurrentSubscribeUnsubscribe()
manager.RemoveListener(Handler);
});
}
-
- Task.WaitAll(tasks);
+ // Act & Assert — no exceptions thrown during concurrent operations
+ var ex = Record.Exception(() => Task.WaitAll(tasks));
+ Assert.Null(ex);
}
[Fact]
public void EventManager_DispatchToMultipleListeners()
{
+ // Arrange
var manager = EventManager.Instance;
int count1 = 0, count2 = 0;
void H1(object? s, System.EventArgs e) => System.Threading.Interlocked.Increment(ref count1);
@@ -86,16 +120,10 @@ public void EventManager_DispatchToMultipleListeners()
manager.AddListener(H1);
manager.AddListener(H2);
- try
- {
- manager.Dispatch(this, System.EventArgs.Empty);
- }
- finally
- {
- manager.RemoveListener(H1);
- manager.RemoveListener(H2);
- }
+ // Act
+ manager.Dispatch(this, System.EventArgs.Empty);
+ // Assert
Assert.Equal(1, count1);
Assert.Equal(1, count2);
}
@@ -103,6 +131,7 @@ public void EventManager_DispatchToMultipleListeners()
[Fact]
public void EventManager_HandlerException_DoesNotBlockOthers()
{
+ // Arrange
var manager = EventManager.Instance;
int count = 0;
void FailingHandler(object? s, System.EventArgs e) => throw new System.InvalidOperationException("handler error");
@@ -111,16 +140,10 @@ public void EventManager_HandlerException_DoesNotBlockOthers()
manager.AddListener(FailingHandler);
manager.AddListener(GoodHandler);
- try
- {
- manager.Dispatch(this, System.EventArgs.Empty);
- }
- finally
- {
- manager.RemoveListener(FailingHandler);
- manager.RemoveListener(GoodHandler);
- }
+ // Act
+ manager.Dispatch(this, System.EventArgs.Empty);
+ // Assert
Assert.Equal(1, count);
}
diff --git a/tests/CoreTest/Event/EventListenerTests.cs b/tests/CoreTest/Event/EventListenerTests.cs
index 0f32f51c..dbffa478 100644
--- a/tests/CoreTest/Event/EventListenerTests.cs
+++ b/tests/CoreTest/Event/EventListenerTests.cs
@@ -6,8 +6,15 @@
namespace CoreTest.Event;
-public class EventListenerTests
+public class EventListenerTests : IDisposable
{
+ /// TearDown: clear singleton state after each test for isolation.
+ public void Dispose()
+ {
+ EventManager.Instance.Clear();
+ GC.SuppressFinalize(this);
+ }
+
private class TestListener : IUpdateEventListener
{
public int AllDownloadCompletedCalls;
diff --git a/tests/CoreTest/Event/EventManagerConcurrencyTests.cs b/tests/CoreTest/Event/EventManagerConcurrencyTests.cs
index 99ff8394..7f3012ee 100644
--- a/tests/CoreTest/Event/EventManagerConcurrencyTests.cs
+++ b/tests/CoreTest/Event/EventManagerConcurrencyTests.cs
@@ -6,8 +6,15 @@
namespace CoreTest.Event;
-public class EventManagerConcurrencyTests
+public class EventManagerConcurrencyTests : IDisposable
{
+ /// TearDown: clear singleton state after each test for isolation.
+ public void Dispose()
+ {
+ EventManager.Instance.Clear();
+ GC.SuppressFinalize(this);
+ }
+
[Fact]
public async Task ConcurrentAddRemoveDispatch_NoExceptions()
{
@@ -66,9 +73,7 @@ public void Dispatch_HandlerException_DoesNotBlockOthers()
Assert.True(handler2Called);
- // Cleanup
- EventManager.Instance.RemoveListener(handler1);
- EventManager.Instance.RemoveListener(handler2);
+
}
[Fact]
@@ -82,6 +87,6 @@ public void AddRemove_Dispatch_DoesNotThrow()
EventManager.Instance.RemoveListener(handler);
EventManager.Instance.Dispatch(this, new ExceptionEventArgs(null, "test"));
- Assert.Equal(1, callCount); // Only first dispatch should trigger
+ Assert.Equal(1, callCount);
}
}
diff --git a/tests/CoreTest/Event/EventManagerTests.cs b/tests/CoreTest/Event/EventManagerTests.cs
index 9a227d5a..8326bd91 100644
--- a/tests/CoreTest/Event/EventManagerTests.cs
+++ b/tests/CoreTest/Event/EventManagerTests.cs
@@ -2,133 +2,183 @@
namespace CoreTest.Event;
-public class EventManagerTests
+///
+/// Unit tests for following AAAT (Arrange-Act-Assert-TearDown).
+/// Implements for explicit TearDown — clears singleton state
+/// after each test to ensure test isolation regardless of test execution order.
+///
+public class EventManagerTests : IDisposable
{
public class TestEventArgs : EventArgs
{
public int Value { get; set; }
}
+ /// TearDown: clear singleton state after each test for isolation.
+ public void Dispose()
+ {
+ EventManager.Instance.Clear();
+ GC.SuppressFinalize(this);
+ }
+
[Fact]
public void AddListener_NullListener_ThrowsArgumentNullException()
{
+ // Arrange & Act & Assert
Assert.Throws(() =>
- EventManager.Instance.AddListener(null));
+ EventManager.Instance.AddListener(null!));
}
[Fact]
public void AddListener_SingleListener_Registered()
{
+ // Arrange
var called = false;
Action