using Apq.Cfg.Changes; using Microsoft.Extensions.Primitives; namespace Apq.Cfg.Tests; /// /// 动态配置重载测试 /// public class DynamicReloadTests : IDisposable { private readonly string _testDir; public DynamicReloadTests() { _testDir = Path.Combine(Path.GetTempPath(), $"ApqCfgDynamicTests_{Guid.NewGuid():N}"); Directory.CreateDirectory(_testDir); } public void Dispose() { if (Directory.Exists(_testDir)) { try { Directory.Delete(_testDir, true); } catch { } } } [Fact] public void ToMicrosoftConfiguration_WithOptions_ReturnsConfiguration() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """{"Key": "Value"}"""); using var cfg = new CfgBuilder() .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true) .Build(); // 先验证静态配置能正常工作 var staticConfig = cfg.ToMicrosoftConfiguration(); Assert.Equal("Value", staticConfig["Key"]); // Act var msConfig = cfg.ToMicrosoftConfiguration(new DynamicReloadOptions { DebounceMs = 50, EnableDynamicReload = true }); // Assert Assert.NotNull(msConfig); Assert.Equal("Value", msConfig["Key"]); } [Fact] public void ToMicrosoftConfiguration_DisabledDynamicReload_ReturnsSameAsStatic() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """{"Key": "Value"}"""); using var cfg = new CfgBuilder() .AddJson(jsonPath, level: 0, writeable: false) .Build(); // Act var staticConfig = cfg.ToMicrosoftConfiguration(); var dynamicConfig = cfg.ToMicrosoftConfiguration(new DynamicReloadOptions { EnableDynamicReload = false }); // Assert Assert.Same(staticConfig, dynamicConfig); } [Fact] public void ToMicrosoftConfiguration_CalledTwice_ReturnsSameInstance() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """{"Key": "Value"}"""); using var cfg = new CfgBuilder() .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true) .Build(); var options = new DynamicReloadOptions { DebounceMs = 50 }; // Act var config1 = cfg.ToMicrosoftConfiguration(options); var config2 = cfg.ToMicrosoftConfiguration(options); // Assert Assert.Same(config1, config2); } [Fact] public void ConfigChanges_IsObservable() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """{"Key": "Value"}"""); using var cfg = new CfgBuilder() .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true) .Build(); // Act & Assert Assert.NotNull(cfg.ConfigChanges); } [Fact] public void MultiLevel_HigherLevelOverrides_InDynamicConfig() { // Arrange var basePath = Path.Combine(_testDir, "base.json"); var overridePath = Path.Combine(_testDir, "override.json"); File.WriteAllText(basePath, """ { "Setting1": "BaseValue1", "Setting2": "BaseValue2" } """); File.WriteAllText(overridePath, """ { "Setting1": "OverrideValue1" } """); using var cfg = new CfgBuilder() .AddJson(basePath, level: 0, writeable: false, reloadOnChange: true) .AddJson(overridePath, level: 1, writeable: false, reloadOnChange: true) .Build(); // Act var msConfig = cfg.ToMicrosoftConfiguration(new DynamicReloadOptions { DebounceMs = 50 }); // Assert Assert.Equal("OverrideValue1", msConfig["Setting1"]); // 被覆盖 Assert.Equal("BaseValue2", msConfig["Setting2"]); // 保持原值 } [Fact] public async Task DynamicConfig_DetectsFileChange() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """{"Key": "OriginalValue"}"""); using var cfg = new CfgBuilder() .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true) .Build(); var msConfig = cfg.ToMicrosoftConfiguration(new DynamicReloadOptions { DebounceMs = 50 }); var changeDetected = new TaskCompletionSource(); var changeToken = msConfig.GetReloadToken(); changeToken.RegisterChangeCallback(_ => changeDetected.TrySetResult(true), null); // Act - 修改文件 await Task.Delay(100); // 等待文件监视器初始化 File.WriteAllText(jsonPath, """{"Key": "NewValue"}"""); // Assert - 等待变更检测(最多 2 秒) var detected = await Task.WhenAny(changeDetected.Task, Task.Delay(2000)) == changeDetected.Task; // 注意:文件变更检测依赖于操作系统的文件监视器,可能不稳定 // 这里主要验证 API 是否正常工作 Assert.NotNull(msConfig.GetReloadToken()); } [Fact] public void DynamicReloadOptions_DefaultValues() { // Arrange & Act var options = new DynamicReloadOptions(); // Assert Assert.Equal(100, options.DebounceMs); Assert.True(options.EnableDynamicReload); Assert.Equal(ReloadStrategy.Eager, options.Strategy); Assert.Null(options.KeyPrefixFilters); Assert.True(options.RollbackOnError); Assert.Equal(0, options.HistorySize); } [Fact] public void ConfigChange_ToString_FormatsCorrectly() { // Arrange var change = new ConfigChange("TestKey", "OldVal", "NewVal", ChangeType.Modified); // Act var str = change.ToString(); // Assert Assert.Contains("Modified", str); Assert.Contains("TestKey", str); Assert.Contains("OldVal", str); Assert.Contains("NewVal", str); } [Fact] public void ConfigChange_NullValues_FormatsCorrectly() { // Arrange var change = new ConfigChange("TestKey", null, "NewVal", ChangeType.Added); // Act var str = change.ToString(); // Assert Assert.Contains("Added", str); Assert.Contains("(null)", str); } [Fact] public void ConfigChangeEvent_Timestamp_IsSet() { // Arrange var before = DateTimeOffset.Now; var changes = new Dictionary { ["Key"] = new ConfigChange("Key", "Old", "New", ChangeType.Modified) }; // Act var evt = new ConfigChangeEvent(changes); var after = DateTimeOffset.Now; // Assert Assert.NotNull(evt.Changes); Assert.Single(evt.Changes); Assert.True(evt.Timestamp >= before && evt.Timestamp <= after); } [Fact] public void ConfigChangeEvent_WithExplicitTimestamp_UsesProvidedValue() { // Arrange var explicitTime = new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero); var changes = new Dictionary { ["Key"] = new ConfigChange("Key", null, "Value", ChangeType.Added) }; // Act var evt = new ConfigChangeEvent(changes, explicitTime); // Assert Assert.Equal(explicitTime, evt.Timestamp); } [Fact] public void ConfigChange_AllChangeTypes_Work() { // Arrange & Act & Assert var added = new ConfigChange("Key", null, "New", ChangeType.Added); Assert.Equal(ChangeType.Added, added.Type); Assert.Null(added.OldValue); Assert.Equal("New", added.NewValue); var modified = new ConfigChange("Key", "Old", "New", ChangeType.Modified); Assert.Equal(ChangeType.Modified, modified.Type); Assert.Equal("Old", modified.OldValue); Assert.Equal("New", modified.NewValue); var removed = new ConfigChange("Key", "Old", null, ChangeType.Removed); Assert.Equal(ChangeType.Removed, removed.Type); Assert.Equal("Old", removed.OldValue); Assert.Null(removed.NewValue); } // ========== 新增测试:变更批次 ID ========== [Fact] public void ConfigChangeEvent_HasUniqueBatchId() { // Arrange var changes = new Dictionary { ["Key"] = new ConfigChange("Key", "Old", "New", ChangeType.Modified) }; // Act var evt1 = new ConfigChangeEvent(changes); var evt2 = new ConfigChangeEvent(changes); // Assert Assert.NotEqual(Guid.Empty, evt1.BatchId); Assert.NotEqual(Guid.Empty, evt2.BatchId); Assert.NotEqual(evt1.BatchId, evt2.BatchId); } [Fact] public void ConfigChangeEvent_WithExplicitBatchId_UsesProvidedValue() { // Arrange var explicitBatchId = Guid.NewGuid(); var changes = new Dictionary { ["Key"] = new ConfigChange("Key", null, "Value", ChangeType.Added) }; // Act var evt = new ConfigChangeEvent(changes, DateTimeOffset.Now, explicitBatchId); // Assert Assert.Equal(explicitBatchId, evt.BatchId); } // ========== 新增测试:重载策略 ========== [Fact] public void ReloadStrategy_AllValuesExist() { // Assert Assert.Equal(0, (int)ReloadStrategy.Eager); Assert.Equal(1, (int)ReloadStrategy.Lazy); Assert.Equal(2, (int)ReloadStrategy.Manual); } [Fact] public void DynamicReloadOptions_CanSetStrategy() { // Arrange & Act var options = new DynamicReloadOptions { Strategy = ReloadStrategy.Manual }; // Assert Assert.Equal(ReloadStrategy.Manual, options.Strategy); } // ========== 新增测试:键前缀过滤器 ========== [Fact] public void DynamicReloadOptions_CanSetKeyPrefixFilters() { // Arrange & Act var options = new DynamicReloadOptions { KeyPrefixFilters = new[] { "Database:", "Logging:" } }; // Assert Assert.NotNull(options.KeyPrefixFilters); Assert.Equal(2, options.KeyPrefixFilters.Count); Assert.Contains("Database:", options.KeyPrefixFilters); Assert.Contains("Logging:", options.KeyPrefixFilters); } // ========== 新增测试:重载错误事件 ========== [Fact] public void ReloadErrorEvent_ContainsAllProperties() { // Arrange var affectedLevels = new HashSet { 0, 1 }; var exception = new InvalidOperationException("Test error"); // Act var evt = new ReloadErrorEvent(affectedLevels, exception, rolledBack: true); // Assert Assert.NotNull(evt.AffectedLevels); Assert.Equal(2, evt.AffectedLevels.Count); Assert.Contains(0, evt.AffectedLevels); Assert.Contains(1, evt.AffectedLevels); Assert.Same(exception, evt.Exception); Assert.True(evt.RolledBack); Assert.True(evt.Timestamp <= DateTimeOffset.Now); } [Fact] public void ReloadErrorEvent_RolledBackFalse_WhenNotRolledBack() { // Arrange var affectedLevels = new HashSet { 0 }; var exception = new Exception("Test"); // Act var evt = new ReloadErrorEvent(affectedLevels, exception, rolledBack: false); // Assert Assert.False(evt.RolledBack); } // ========== 新增测试:变更历史 ========== [Fact] public void DynamicReloadOptions_CanSetHistorySize() { // Arrange & Act var options = new DynamicReloadOptions { HistorySize = 10 }; // Assert Assert.Equal(10, options.HistorySize); } // ========== 新增测试:回滚选项 ========== [Fact] public void DynamicReloadOptions_RollbackOnError_DefaultTrue() { // Arrange & Act var options = new DynamicReloadOptions(); // Assert Assert.True(options.RollbackOnError); } [Fact] public void DynamicReloadOptions_CanDisableRollback() { // Arrange & Act var options = new DynamicReloadOptions { RollbackOnError = false }; // Assert Assert.False(options.RollbackOnError); } }