ConfigChangesSubscriptionTests.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. using System.Reactive.Linq;
  2. using Apq.Cfg.Changes;
  3. namespace Apq.Cfg.Tests;
  4. /// <summary>
  5. /// ConfigChanges 订阅测试
  6. /// </summary>
  7. public class ConfigChangesSubscriptionTests : IDisposable
  8. {
  9. private readonly string _testDir;
  10. public ConfigChangesSubscriptionTests()
  11. {
  12. _testDir = Path.Combine(Path.GetTempPath(), $"ApqCfgSubscriptionTests_{Guid.NewGuid():N}");
  13. Directory.CreateDirectory(_testDir);
  14. }
  15. public void Dispose()
  16. {
  17. if (Directory.Exists(_testDir))
  18. {
  19. try { Directory.Delete(_testDir, true); }
  20. catch { }
  21. }
  22. }
  23. #region 基本订阅测试
  24. [Fact]
  25. public void ConfigChanges_IsNotNull()
  26. {
  27. // Arrange
  28. var jsonPath = Path.Combine(_testDir, "config.json");
  29. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  30. using var cfg = new CfgBuilder()
  31. .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
  32. .Build();
  33. // Act & Assert
  34. Assert.NotNull(cfg.ConfigChanges);
  35. }
  36. [Fact]
  37. public void ConfigChanges_CanSubscribe()
  38. {
  39. // Arrange
  40. var jsonPath = Path.Combine(_testDir, "config.json");
  41. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  42. using var cfg = new CfgBuilder()
  43. .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
  44. .Build();
  45. // Act
  46. using var subscription = cfg.ConfigChanges.Subscribe(_ => { });
  47. // Assert - 订阅应该成功
  48. Assert.NotNull(subscription);
  49. }
  50. [Fact]
  51. public void ConfigChanges_MultipleSubscribers_AllSubscribeSuccessfully()
  52. {
  53. // Arrange
  54. var jsonPath = Path.Combine(_testDir, "config.json");
  55. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  56. using var cfg = new CfgBuilder()
  57. .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
  58. .Build();
  59. // Act - 多个订阅者
  60. using var sub1 = cfg.ConfigChanges.Subscribe(_ => { });
  61. using var sub2 = cfg.ConfigChanges.Subscribe(_ => { });
  62. using var sub3 = cfg.ConfigChanges.Subscribe(_ => { });
  63. // Assert - 所有订阅都应该成功
  64. Assert.NotNull(sub1);
  65. Assert.NotNull(sub2);
  66. Assert.NotNull(sub3);
  67. }
  68. [Fact]
  69. public void ConfigChanges_Unsubscribe_Works()
  70. {
  71. // Arrange
  72. var jsonPath = Path.Combine(_testDir, "config.json");
  73. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  74. using var cfg = new CfgBuilder()
  75. .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
  76. .Build();
  77. var eventCount = 0;
  78. // Act
  79. var subscription = cfg.ConfigChanges.Subscribe(_ => eventCount++);
  80. subscription.Dispose(); // 取消订阅
  81. // Assert - 取消订阅不应抛出异常
  82. }
  83. #endregion
  84. #region ConfigChangeEvent 测试
  85. [Fact]
  86. public void ConfigChangeEvent_HasCorrectTimestamp()
  87. {
  88. // Arrange
  89. var before = DateTimeOffset.Now;
  90. var changes = new Dictionary<string, ConfigChange>
  91. {
  92. ["Key"] = new ConfigChange("Key", "Old", "New", ChangeType.Modified)
  93. };
  94. // Act
  95. var evt = new ConfigChangeEvent(changes);
  96. var after = DateTimeOffset.Now;
  97. // Assert
  98. Assert.True(evt.Timestamp >= before);
  99. Assert.True(evt.Timestamp <= after);
  100. }
  101. [Fact]
  102. public void ConfigChangeEvent_WithExplicitTimestamp_Works()
  103. {
  104. // Arrange
  105. var explicitTime = new DateTimeOffset(2024, 6, 15, 12, 0, 0, TimeSpan.Zero);
  106. var changes = new Dictionary<string, ConfigChange>
  107. {
  108. ["Key"] = new ConfigChange("Key", null, "Value", ChangeType.Added)
  109. };
  110. // Act
  111. var evt = new ConfigChangeEvent(changes, explicitTime);
  112. // Assert
  113. Assert.Equal(explicitTime, evt.Timestamp);
  114. }
  115. [Fact]
  116. public void ConfigChangeEvent_Changes_IsReadOnly()
  117. {
  118. // Arrange
  119. var changes = new Dictionary<string, ConfigChange>
  120. {
  121. ["Key1"] = new ConfigChange("Key1", null, "Value1", ChangeType.Added),
  122. ["Key2"] = new ConfigChange("Key2", "Old", "New", ChangeType.Modified)
  123. };
  124. // Act
  125. var evt = new ConfigChangeEvent(changes);
  126. // Assert
  127. Assert.Equal(2, evt.Changes.Count);
  128. Assert.True(evt.Changes.ContainsKey("Key1"));
  129. Assert.True(evt.Changes.ContainsKey("Key2"));
  130. }
  131. [Fact]
  132. public void ConfigChangeEvent_EmptyChanges_Works()
  133. {
  134. // Arrange
  135. var changes = new Dictionary<string, ConfigChange>();
  136. // Act
  137. var evt = new ConfigChangeEvent(changes);
  138. // Assert
  139. Assert.Empty(evt.Changes);
  140. }
  141. #endregion
  142. #region ConfigChange 测试
  143. [Fact]
  144. public void ConfigChange_Added_HasCorrectProperties()
  145. {
  146. // Arrange & Act
  147. var change = new ConfigChange("NewKey", null, "NewValue", ChangeType.Added);
  148. // Assert
  149. Assert.Equal("NewKey", change.Key);
  150. Assert.Null(change.OldValue);
  151. Assert.Equal("NewValue", change.NewValue);
  152. Assert.Equal(ChangeType.Added, change.Type);
  153. }
  154. [Fact]
  155. public void ConfigChange_Modified_HasCorrectProperties()
  156. {
  157. // Arrange & Act
  158. var change = new ConfigChange("Key", "OldValue", "NewValue", ChangeType.Modified);
  159. // Assert
  160. Assert.Equal("Key", change.Key);
  161. Assert.Equal("OldValue", change.OldValue);
  162. Assert.Equal("NewValue", change.NewValue);
  163. Assert.Equal(ChangeType.Modified, change.Type);
  164. }
  165. [Fact]
  166. public void ConfigChange_Removed_HasCorrectProperties()
  167. {
  168. // Arrange & Act
  169. var change = new ConfigChange("RemovedKey", "OldValue", null, ChangeType.Removed);
  170. // Assert
  171. Assert.Equal("RemovedKey", change.Key);
  172. Assert.Equal("OldValue", change.OldValue);
  173. Assert.Null(change.NewValue);
  174. Assert.Equal(ChangeType.Removed, change.Type);
  175. }
  176. [Fact]
  177. public void ConfigChange_ToString_ContainsAllInfo()
  178. {
  179. // Arrange
  180. var change = new ConfigChange("TestKey", "OldVal", "NewVal", ChangeType.Modified);
  181. // Act
  182. var str = change.ToString();
  183. // Assert
  184. Assert.Contains("TestKey", str);
  185. Assert.Contains("OldVal", str);
  186. Assert.Contains("NewVal", str);
  187. Assert.Contains("Modified", str);
  188. }
  189. [Fact]
  190. public void ConfigChange_ToString_HandlesNullValues()
  191. {
  192. // Arrange
  193. var change = new ConfigChange("Key", null, "Value", ChangeType.Added);
  194. // Act
  195. var str = change.ToString();
  196. // Assert
  197. Assert.Contains("(null)", str);
  198. Assert.Contains("Value", str);
  199. }
  200. [Fact]
  201. public void ConfigChange_Equality_WorksCorrectly()
  202. {
  203. // Arrange
  204. var change1 = new ConfigChange("Key", "Old", "New", ChangeType.Modified);
  205. var change2 = new ConfigChange("Key", "Old", "New", ChangeType.Modified);
  206. var change3 = new ConfigChange("Key", "Old", "Different", ChangeType.Modified);
  207. // Assert - struct 默认相等性比较
  208. Assert.Equal(change1.Key, change2.Key);
  209. Assert.Equal(change1.OldValue, change2.OldValue);
  210. Assert.Equal(change1.NewValue, change2.NewValue);
  211. Assert.NotEqual(change1.NewValue, change3.NewValue);
  212. }
  213. #endregion
  214. #region ChangeType 测试
  215. [Fact]
  216. public void ChangeType_AllValues_AreDefined()
  217. {
  218. // Assert
  219. Assert.True(Enum.IsDefined(typeof(ChangeType), ChangeType.Added));
  220. Assert.True(Enum.IsDefined(typeof(ChangeType), ChangeType.Modified));
  221. Assert.True(Enum.IsDefined(typeof(ChangeType), ChangeType.Removed));
  222. }
  223. [Fact]
  224. public void ChangeType_Values_AreDistinct()
  225. {
  226. // Assert
  227. Assert.NotEqual(ChangeType.Added, ChangeType.Modified);
  228. Assert.NotEqual(ChangeType.Modified, ChangeType.Removed);
  229. Assert.NotEqual(ChangeType.Added, ChangeType.Removed);
  230. }
  231. #endregion
  232. #region DynamicReloadOptions 测试
  233. [Fact]
  234. public void DynamicReloadOptions_DefaultValues()
  235. {
  236. // Arrange & Act
  237. var options = new DynamicReloadOptions();
  238. // Assert
  239. Assert.Equal(100, options.DebounceMs);
  240. Assert.True(options.EnableDynamicReload);
  241. }
  242. [Fact]
  243. public void DynamicReloadOptions_CanSetDebounceMs()
  244. {
  245. // Arrange & Act
  246. var options = new DynamicReloadOptions { DebounceMs = 500 };
  247. // Assert
  248. Assert.Equal(500, options.DebounceMs);
  249. }
  250. [Fact]
  251. public void DynamicReloadOptions_CanDisableDynamicReload()
  252. {
  253. // Arrange & Act
  254. var options = new DynamicReloadOptions { EnableDynamicReload = false };
  255. // Assert
  256. Assert.False(options.EnableDynamicReload);
  257. }
  258. [Fact]
  259. public void DynamicReloadOptions_ZeroDebounceMs_Works()
  260. {
  261. // Arrange & Act
  262. var options = new DynamicReloadOptions { DebounceMs = 0 };
  263. // Assert
  264. Assert.Equal(0, options.DebounceMs);
  265. }
  266. #endregion
  267. #region 动态配置集成测试
  268. [Fact]
  269. public void ToMicrosoftConfiguration_WithDynamicReload_ReturnsConfiguration()
  270. {
  271. // Arrange
  272. var jsonPath = Path.Combine(_testDir, "config.json");
  273. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  274. using var cfg = new CfgBuilder()
  275. .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
  276. .Build();
  277. // Act
  278. var msConfig = cfg.ToMicrosoftConfiguration(new DynamicReloadOptions
  279. {
  280. DebounceMs = 50,
  281. EnableDynamicReload = true
  282. });
  283. // Assert
  284. Assert.NotNull(msConfig);
  285. Assert.Equal("Value", msConfig["Key"]);
  286. }
  287. [Fact]
  288. public void ToMicrosoftConfiguration_DisabledReload_ReturnsSameAsStatic()
  289. {
  290. // Arrange
  291. var jsonPath = Path.Combine(_testDir, "config.json");
  292. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  293. using var cfg = new CfgBuilder()
  294. .AddJson(jsonPath, level: 0, writeable: false)
  295. .Build();
  296. // Act
  297. var staticConfig = cfg.ToMicrosoftConfiguration();
  298. var dynamicConfig = cfg.ToMicrosoftConfiguration(new DynamicReloadOptions
  299. {
  300. EnableDynamicReload = false
  301. });
  302. // Assert
  303. Assert.Same(staticConfig, dynamicConfig);
  304. }
  305. [Fact]
  306. public void ToMicrosoftConfiguration_CalledMultipleTimes_ReturnsSameInstance()
  307. {
  308. // Arrange
  309. var jsonPath = Path.Combine(_testDir, "config.json");
  310. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  311. using var cfg = new CfgBuilder()
  312. .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
  313. .Build();
  314. var options = new DynamicReloadOptions { DebounceMs = 50 };
  315. // Act
  316. var config1 = cfg.ToMicrosoftConfiguration(options);
  317. var config2 = cfg.ToMicrosoftConfiguration(options);
  318. // Assert
  319. Assert.Same(config1, config2);
  320. }
  321. [Fact]
  322. public void ToMicrosoftConfiguration_NullOptions_UsesDefaults()
  323. {
  324. // Arrange
  325. var jsonPath = Path.Combine(_testDir, "config.json");
  326. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  327. using var cfg = new CfgBuilder()
  328. .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
  329. .Build();
  330. // Act
  331. var msConfig = cfg.ToMicrosoftConfiguration(null);
  332. // Assert
  333. Assert.NotNull(msConfig);
  334. Assert.Equal("Value", msConfig["Key"]);
  335. }
  336. #endregion
  337. #region Dispose 后订阅测试
  338. [Fact]
  339. public void ConfigChanges_AfterDispose_CompletesObservable()
  340. {
  341. // Arrange
  342. var jsonPath = Path.Combine(_testDir, "config.json");
  343. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  344. var cfg = new CfgBuilder()
  345. .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
  346. .Build();
  347. var completed = false;
  348. using var subscription = cfg.ConfigChanges.Subscribe(
  349. onNext: _ => { },
  350. onCompleted: () => completed = true
  351. );
  352. // Act
  353. cfg.Dispose();
  354. // Assert - Observable 应该完成
  355. Assert.True(completed);
  356. }
  357. [Fact]
  358. public async Task ConfigChanges_AfterDisposeAsync_CompletesObservable()
  359. {
  360. // Arrange
  361. var jsonPath = Path.Combine(_testDir, "config.json");
  362. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  363. var cfg = new CfgBuilder()
  364. .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
  365. .Build();
  366. var completed = false;
  367. using var subscription = cfg.ConfigChanges.Subscribe(
  368. onNext: _ => { },
  369. onCompleted: () => completed = true
  370. );
  371. // Act
  372. await cfg.DisposeAsync();
  373. // Assert
  374. Assert.True(completed);
  375. }
  376. #endregion
  377. #region 文件变更检测测试
  378. [Fact]
  379. public void DynamicConfig_HasReloadToken()
  380. {
  381. // Arrange
  382. var jsonPath = Path.Combine(_testDir, "config.json");
  383. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  384. using var cfg = new CfgBuilder()
  385. .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
  386. .Build();
  387. var msConfig = cfg.ToMicrosoftConfiguration(new DynamicReloadOptions
  388. {
  389. DebounceMs = 50
  390. });
  391. // Act
  392. var reloadToken = msConfig.GetReloadToken();
  393. // Assert
  394. Assert.NotNull(reloadToken);
  395. Assert.False(reloadToken.HasChanged); // 初始状态未变更
  396. }
  397. [Fact]
  398. public async Task DynamicConfig_FileChange_TriggersReload()
  399. {
  400. // Arrange
  401. var jsonPath = Path.Combine(_testDir, "config.json");
  402. File.WriteAllText(jsonPath, """{"Key": "OriginalValue"}""");
  403. using var cfg = new CfgBuilder()
  404. .AddJson(jsonPath, level: 0, writeable: false, reloadOnChange: true)
  405. .Build();
  406. var msConfig = cfg.ToMicrosoftConfiguration(new DynamicReloadOptions
  407. {
  408. DebounceMs = 50
  409. });
  410. var changeDetected = new TaskCompletionSource<bool>();
  411. var token = msConfig.GetReloadToken();
  412. token.RegisterChangeCallback(_ => changeDetected.TrySetResult(true), null);
  413. // Act - 修改文件
  414. await Task.Delay(100); // 等待文件监视器初始化
  415. File.WriteAllText(jsonPath, """{"Key": "NewValue"}""");
  416. // Assert - 等待变更检测(最多 3 秒)
  417. var detected = await Task.WhenAny(changeDetected.Task, Task.Delay(3000)) == changeDetected.Task;
  418. // 注意:文件变更检测依赖于操作系统,可能不稳定
  419. // 主要验证 API 正常工作
  420. Assert.NotNull(msConfig.GetReloadToken());
  421. }
  422. #endregion
  423. }