using System.Reactive.Linq; using Apq.Cfg.Changes; namespace Apq.Cfg.Tests; /// /// ConfigChanges 订阅测试 /// public class ConfigChangesSubscriptionTests : IDisposable { private readonly string _testDir; public ConfigChangesSubscriptionTests() { _testDir = Path.Combine(Path.GetTempPath(), $"ApqCfgSubscriptionTests_{Guid.NewGuid():N}"); Directory.CreateDirectory(_testDir); } public void Dispose() { if (Directory.Exists(_testDir)) { try { Directory.Delete(_testDir, true); } catch { } } } #region 基本订阅测试 [Fact] public void ConfigChanges_IsNotNull() { // 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 ConfigChanges_CanSubscribe() { // 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 using var subscription = cfg.ConfigChanges.Subscribe(_ => { }); // Assert - 订阅应该成功 Assert.NotNull(subscription); } [Fact] public void ConfigChanges_MultipleSubscribers_AllSubscribeSuccessfully() { // 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 - 多个订阅者 using var sub1 = cfg.ConfigChanges.Subscribe(_ => { }); using var sub2 = cfg.ConfigChanges.Subscribe(_ => { }); using var sub3 = cfg.ConfigChanges.Subscribe(_ => { }); // Assert - 所有订阅都应该成功 Assert.NotNull(sub1); Assert.NotNull(sub2); Assert.NotNull(sub3); } [Fact] public void ConfigChanges_Unsubscribe_Works() { // 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 eventCount = 0; // Act var subscription = cfg.ConfigChanges.Subscribe(_ => eventCount++); subscription.Dispose(); // 取消订阅 // Assert - 取消订阅不应抛出异常 } #endregion #region ConfigChangeEvent 测试 [Fact] public void ConfigChangeEvent_HasCorrectTimestamp() { // 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.True(evt.Timestamp >= before); Assert.True(evt.Timestamp <= after); } [Fact] public void ConfigChangeEvent_WithExplicitTimestamp_Works() { // Arrange var explicitTime = new DateTimeOffset(2024, 6, 15, 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 ConfigChangeEvent_Changes_IsReadOnly() { // Arrange var changes = new Dictionary { ["Key1"] = new ConfigChange("Key1", null, "Value1", ChangeType.Added), ["Key2"] = new ConfigChange("Key2", "Old", "New", ChangeType.Modified) }; // Act var evt = new ConfigChangeEvent(changes); // Assert Assert.Equal(2, evt.Changes.Count); Assert.True(evt.Changes.ContainsKey("Key1")); Assert.True(evt.Changes.ContainsKey("Key2")); } [Fact] public void ConfigChangeEvent_EmptyChanges_Works() { // Arrange var changes = new Dictionary(); // Act var evt = new ConfigChangeEvent(changes); // Assert Assert.Empty(evt.Changes); } #endregion #region ConfigChange 测试 [Fact] public void ConfigChange_Added_HasCorrectProperties() { // Arrange & Act var change = new ConfigChange("NewKey", null, "NewValue", ChangeType.Added); // Assert Assert.Equal("NewKey", change.Key); Assert.Null(change.OldValue); Assert.Equal("NewValue", change.NewValue); Assert.Equal(ChangeType.Added, change.Type); } [Fact] public void ConfigChange_Modified_HasCorrectProperties() { // Arrange & Act var change = new ConfigChange("Key", "OldValue", "NewValue", ChangeType.Modified); // Assert Assert.Equal("Key", change.Key); Assert.Equal("OldValue", change.OldValue); Assert.Equal("NewValue", change.NewValue); Assert.Equal(ChangeType.Modified, change.Type); } [Fact] public void ConfigChange_Removed_HasCorrectProperties() { // Arrange & Act var change = new ConfigChange("RemovedKey", "OldValue", null, ChangeType.Removed); // Assert Assert.Equal("RemovedKey", change.Key); Assert.Equal("OldValue", change.OldValue); Assert.Null(change.NewValue); Assert.Equal(ChangeType.Removed, change.Type); } [Fact] public void ConfigChange_ToString_ContainsAllInfo() { // Arrange var change = new ConfigChange("TestKey", "OldVal", "NewVal", ChangeType.Modified); // Act var str = change.ToString(); // Assert Assert.Contains("TestKey", str); Assert.Contains("OldVal", str); Assert.Contains("NewVal", str); Assert.Contains("Modified", str); } [Fact] public void ConfigChange_ToString_HandlesNullValues() { // Arrange var change = new ConfigChange("Key", null, "Value", ChangeType.Added); // Act var str = change.ToString(); // Assert Assert.Contains("(null)", str); Assert.Contains("Value", str); } [Fact] public void ConfigChange_Equality_WorksCorrectly() { // Arrange var change1 = new ConfigChange("Key", "Old", "New", ChangeType.Modified); var change2 = new ConfigChange("Key", "Old", "New", ChangeType.Modified); var change3 = new ConfigChange("Key", "Old", "Different", ChangeType.Modified); // Assert - struct 默认相等性比较 Assert.Equal(change1.Key, change2.Key); Assert.Equal(change1.OldValue, change2.OldValue); Assert.Equal(change1.NewValue, change2.NewValue); Assert.NotEqual(change1.NewValue, change3.NewValue); } #endregion #region ChangeType 测试 [Fact] public void ChangeType_AllValues_AreDefined() { // Assert Assert.True(Enum.IsDefined(typeof(ChangeType), ChangeType.Added)); Assert.True(Enum.IsDefined(typeof(ChangeType), ChangeType.Modified)); Assert.True(Enum.IsDefined(typeof(ChangeType), ChangeType.Removed)); } [Fact] public void ChangeType_Values_AreDistinct() { // Assert Assert.NotEqual(ChangeType.Added, ChangeType.Modified); Assert.NotEqual(ChangeType.Modified, ChangeType.Removed); Assert.NotEqual(ChangeType.Added, ChangeType.Removed); } #endregion #region DynamicReloadOptions 测试 [Fact] public void DynamicReloadOptions_DefaultValues() { // Arrange & Act var options = new DynamicReloadOptions(); // Assert Assert.Equal(100, options.DebounceMs); Assert.True(options.EnableDynamicReload); } [Fact] public void DynamicReloadOptions_CanSetDebounceMs() { // Arrange & Act var options = new DynamicReloadOptions { DebounceMs = 500 }; // Assert Assert.Equal(500, options.DebounceMs); } [Fact] public void DynamicReloadOptions_CanDisableDynamicReload() { // Arrange & Act var options = new DynamicReloadOptions { EnableDynamicReload = false }; // Assert Assert.False(options.EnableDynamicReload); } [Fact] public void DynamicReloadOptions_ZeroDebounceMs_Works() { // Arrange & Act var options = new DynamicReloadOptions { DebounceMs = 0 }; // Assert Assert.Equal(0, options.DebounceMs); } #endregion #region 动态配置集成测试 [Fact] public void ToMicrosoftConfiguration_WithDynamicReload_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(); // Act var msConfig = cfg.ToMicrosoftConfiguration(new DynamicReloadOptions { DebounceMs = 50, EnableDynamicReload = true }); // Assert Assert.NotNull(msConfig); Assert.Equal("Value", msConfig["Key"]); } [Fact] public void ToMicrosoftConfiguration_DisabledReload_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_CalledMultipleTimes_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 ToMicrosoftConfiguration_NullOptions_UsesDefaults() { // 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 var msConfig = cfg.ToMicrosoftConfiguration(null); // Assert Assert.NotNull(msConfig); Assert.Equal("Value", msConfig["Key"]); } #endregion #region Dispose 后订阅测试 [Fact] public void ConfigChanges_AfterDispose_CompletesObservable() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """{"Key": "Value"}"""); var cfg = new CfgBuilder() .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true) .Build(); var completed = false; using var subscription = cfg.ConfigChanges.Subscribe( onNext: _ => { }, onCompleted: () => completed = true ); // Act cfg.Dispose(); // Assert - Observable 应该完成 Assert.True(completed); } [Fact] public async Task ConfigChanges_AfterDisposeAsync_CompletesObservable() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """{"Key": "Value"}"""); var cfg = new CfgBuilder() .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true) .Build(); var completed = false; using var subscription = cfg.ConfigChanges.Subscribe( onNext: _ => { }, onCompleted: () => completed = true ); // Act await cfg.DisposeAsync(); // Assert Assert.True(completed); } #endregion #region 文件变更检测测试 [Fact] public void DynamicConfig_HasReloadToken() { // 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 msConfig = cfg.ToMicrosoftConfiguration(new DynamicReloadOptions { DebounceMs = 50 }); // Act var reloadToken = msConfig.GetReloadToken(); // Assert Assert.NotNull(reloadToken); Assert.False(reloadToken.HasChanged); // 初始状态未变更 } [Fact] public async Task DynamicConfig_FileChange_TriggersReload() { // 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 token = msConfig.GetReloadToken(); token.RegisterChangeCallback(_ => changeDetected.TrySetResult(true), null); // Act - 修改文件 await Task.Delay(100); // 等待文件监视器初始化 File.WriteAllText(jsonPath, """{"Key": "NewValue"}"""); // Assert - 等待变更检测(最多 3 秒) var detected = await Task.WhenAny(changeDetected.Task, Task.Delay(3000)) == changeDetected.Task; // 注意:文件变更检测依赖于操作系统,可能不稳定 // 主要验证 API 正常工作 Assert.NotNull(msConfig.GetReloadToken()); } #endregion }