ExceptionHandlingTests.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. namespace Apq.Cfg.Tests;
  2. /// <summary>
  3. /// 异常场景测试
  4. /// </summary>
  5. public class ExceptionHandlingTests : IDisposable
  6. {
  7. private readonly string _testDir;
  8. public ExceptionHandlingTests()
  9. {
  10. _testDir = Path.Combine(Path.GetTempPath(), $"ApqCfgExceptionTests_{Guid.NewGuid():N}");
  11. Directory.CreateDirectory(_testDir);
  12. }
  13. public void Dispose()
  14. {
  15. if (Directory.Exists(_testDir))
  16. {
  17. try { Directory.Delete(_testDir, true); }
  18. catch { }
  19. }
  20. }
  21. #region 无效 JSON 测试
  22. [Fact]
  23. public void Build_InvalidJson_ThrowsException()
  24. {
  25. // Arrange
  26. var jsonPath = Path.Combine(_testDir, "invalid.json");
  27. File.WriteAllText(jsonPath, "{ invalid json }");
  28. // Act & Assert
  29. Assert.ThrowsAny<Exception>(() =>
  30. {
  31. using var cfg = new CfgBuilder()
  32. .AddJson(jsonPath, level: 0, writeable: false, optional: false)
  33. .Build();
  34. });
  35. }
  36. [Fact]
  37. public void Build_TruncatedJson_ThrowsException()
  38. {
  39. // Arrange
  40. var jsonPath = Path.Combine(_testDir, "truncated.json");
  41. File.WriteAllText(jsonPath, """{"Key": "Value"""); // 缺少结束括号
  42. // Act & Assert
  43. Assert.ThrowsAny<Exception>(() =>
  44. {
  45. using var cfg = new CfgBuilder()
  46. .AddJson(jsonPath, level: 0, writeable: false, optional: false)
  47. .Build();
  48. });
  49. }
  50. #endregion
  51. #region 无可写源测试
  52. [Fact]
  53. public void Set_NoWritableSource_ThrowsInvalidOperationException()
  54. {
  55. // Arrange
  56. var jsonPath = Path.Combine(_testDir, "readonly.json");
  57. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  58. using var cfg = new CfgBuilder()
  59. .AddJson(jsonPath, level: 0, writeable: false)
  60. .Build();
  61. // Act & Assert
  62. var ex = Assert.Throws<InvalidOperationException>(() => cfg.Set("NewKey", "NewValue"));
  63. Assert.Contains("没有可写的配置源", ex.Message);
  64. }
  65. [Fact]
  66. public void Remove_NoWritableSource_ThrowsInvalidOperationException()
  67. {
  68. // Arrange
  69. var jsonPath = Path.Combine(_testDir, "readonly.json");
  70. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  71. using var cfg = new CfgBuilder()
  72. .AddJson(jsonPath, level: 0, writeable: false)
  73. .Build();
  74. // Act & Assert
  75. var ex = Assert.Throws<InvalidOperationException>(() => cfg.Remove("Key"));
  76. Assert.Contains("没有可写的配置源", ex.Message);
  77. }
  78. [Fact]
  79. public void Set_InvalidTargetLevel_ThrowsInvalidOperationException()
  80. {
  81. // Arrange
  82. var jsonPath = Path.Combine(_testDir, "config.json");
  83. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  84. using var cfg = new CfgBuilder()
  85. .AddJson(jsonPath, level: 0, writeable: true, isPrimaryWriter: true)
  86. .Build();
  87. // Act & Assert - 指定不存在的层级
  88. var ex = Assert.Throws<InvalidOperationException>(() => cfg.Set("Key", "Value", targetLevel: 999));
  89. Assert.Contains("没有可写的配置源", ex.Message);
  90. }
  91. [Fact]
  92. public void Remove_InvalidTargetLevel_ThrowsInvalidOperationException()
  93. {
  94. // Arrange
  95. var jsonPath = Path.Combine(_testDir, "config.json");
  96. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  97. using var cfg = new CfgBuilder()
  98. .AddJson(jsonPath, level: 0, writeable: true, isPrimaryWriter: true)
  99. .Build();
  100. // Act & Assert
  101. var ex = Assert.Throws<InvalidOperationException>(() => cfg.Remove("Key", targetLevel: 999));
  102. Assert.Contains("没有可写的配置源", ex.Message);
  103. }
  104. #endregion
  105. #region 类型转换异常测试
  106. [Fact]
  107. public void Get_InvalidIntConversion_ThrowsException()
  108. {
  109. // Arrange
  110. var jsonPath = Path.Combine(_testDir, "config.json");
  111. File.WriteAllText(jsonPath, """{"NotANumber": "abc"}""");
  112. using var cfg = new CfgBuilder()
  113. .AddJson(jsonPath, level: 0, writeable: false)
  114. .Build();
  115. // Act & Assert
  116. Assert.Throws<InvalidOperationException>(() => cfg.Get<int>("NotANumber"));
  117. }
  118. [Fact]
  119. public void Get_InvalidBoolConversion_ThrowsException()
  120. {
  121. // Arrange
  122. var jsonPath = Path.Combine(_testDir, "config.json");
  123. File.WriteAllText(jsonPath, """{"NotABool": "maybe"}""");
  124. using var cfg = new CfgBuilder()
  125. .AddJson(jsonPath, level: 0, writeable: false)
  126. .Build();
  127. // Act & Assert - Microsoft.Extensions.Configuration 对无效 bool 值抛出异常
  128. Assert.Throws<InvalidOperationException>(() => cfg.Get<bool>("NotABool"));
  129. }
  130. [Fact]
  131. public void Get_OverflowInt_ThrowsException()
  132. {
  133. // Arrange
  134. var jsonPath = Path.Combine(_testDir, "config.json");
  135. // 使用字符串形式的大数字
  136. File.WriteAllText(jsonPath, """{"TooBig": "99999999999999999999"}""");
  137. using var cfg = new CfgBuilder()
  138. .AddJson(jsonPath, level: 0, writeable: false)
  139. .Build();
  140. // Act & Assert - 溢出时抛出异常
  141. Assert.Throws<InvalidOperationException>(() => cfg.Get<int>("TooBig"));
  142. }
  143. [Fact]
  144. public void GetRequired_NonExistentKey_ThrowsInvalidOperationException()
  145. {
  146. // Arrange
  147. var jsonPath = Path.Combine(_testDir, "config.json");
  148. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  149. using var cfg = new CfgBuilder()
  150. .AddJson(jsonPath, level: 0, writeable: false)
  151. .Build();
  152. // Act & Assert
  153. var ex = Assert.Throws<InvalidOperationException>(() => cfg.GetRequired<string>("NonExistent"));
  154. Assert.Contains("NonExistent", ex.Message);
  155. Assert.Contains("必需的配置键", ex.Message);
  156. }
  157. #endregion
  158. #region 文件权限测试
  159. [Fact]
  160. public async Task SaveAsync_ReadOnlyFile_ThrowsException()
  161. {
  162. // Arrange
  163. var jsonPath = Path.Combine(_testDir, "readonly.json");
  164. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  165. // 设置文件为只读
  166. var fileInfo = new FileInfo(jsonPath);
  167. fileInfo.IsReadOnly = true;
  168. try
  169. {
  170. using var cfg = new CfgBuilder()
  171. .AddJson(jsonPath, level: 0, writeable: true, isPrimaryWriter: true)
  172. .Build();
  173. cfg.Set("NewKey", "NewValue");
  174. // Act & Assert - 保存到只读文件应该抛出异常
  175. var exception = await Assert.ThrowsAnyAsync<Exception>(async () => await cfg.SaveAsync());
  176. // 验证是 IO 相关的异常
  177. Assert.True(exception is IOException || exception is UnauthorizedAccessException,
  178. $"Expected IOException or UnauthorizedAccessException, but got {exception.GetType().Name}");
  179. }
  180. finally
  181. {
  182. // 清理:移除只读属性
  183. fileInfo.IsReadOnly = false;
  184. }
  185. }
  186. #endregion
  187. #region Dispose 后操作测试
  188. [Fact]
  189. public void Get_AfterDispose_MayThrowOrReturnNull()
  190. {
  191. // Arrange
  192. var jsonPath = Path.Combine(_testDir, "config.json");
  193. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  194. var cfg = new CfgBuilder()
  195. .AddJson(jsonPath, level: 0, writeable: false)
  196. .Build();
  197. cfg.Dispose();
  198. // Act & Assert - Dispose 后的行为取决于实现
  199. // 可能抛出 ObjectDisposedException 或返回 null
  200. try
  201. {
  202. var value = cfg.Get("Key");
  203. // 如果没有抛出异常,值可能是 null 或原值
  204. }
  205. catch (ObjectDisposedException)
  206. {
  207. // 预期的异常
  208. }
  209. }
  210. [Fact]
  211. public async Task SaveAsync_AfterDispose_MayThrowOrDoNothing()
  212. {
  213. // Arrange
  214. var jsonPath = Path.Combine(_testDir, "config.json");
  215. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  216. var cfg = new CfgBuilder()
  217. .AddJson(jsonPath, level: 0, writeable: true, isPrimaryWriter: true)
  218. .Build();
  219. cfg.Set("NewKey", "NewValue");
  220. cfg.Dispose();
  221. // Act & Assert
  222. try
  223. {
  224. await cfg.SaveAsync();
  225. // 如果没有抛出异常,操作可能被忽略
  226. }
  227. catch (ObjectDisposedException)
  228. {
  229. // 预期的异常
  230. }
  231. catch (InvalidOperationException)
  232. {
  233. // 也可能抛出此异常
  234. }
  235. }
  236. #endregion
  237. #region 空配置源测试
  238. [Fact]
  239. public void Build_NoSources_ReturnsEmptyConfig()
  240. {
  241. // Act - 没有添加任何配置源,应该返回空配置而不是抛出异常
  242. using var cfg = new CfgBuilder().Build();
  243. // Assert - 空配置应该正常工作
  244. Assert.Null(cfg.Get("AnyKey"));
  245. Assert.False(cfg.Exists("AnyKey"));
  246. }
  247. #endregion
  248. #region 多 PrimaryWriter 测试
  249. [Fact]
  250. public void Build_MultiplePrimaryWriters_SameLevelUsesLast()
  251. {
  252. // Arrange
  253. var path1 = Path.Combine(_testDir, "config1.json");
  254. var path2 = Path.Combine(_testDir, "config2.json");
  255. File.WriteAllText(path1, """{"Key": "Value1"}""");
  256. File.WriteAllText(path2, """{"Key": "Value2"}""");
  257. // Act - 同一层级多个 PrimaryWriter
  258. using var cfg = new CfgBuilder()
  259. .AddJson(path1, level: 0, writeable: true, isPrimaryWriter: true)
  260. .AddJson(path2, level: 0, writeable: true, isPrimaryWriter: true)
  261. .Build();
  262. // Assert - 应该使用最后一个
  263. cfg.Set("NewKey", "NewValue");
  264. Assert.Equal("NewValue", cfg.Get("NewKey"));
  265. }
  266. [Fact]
  267. public async Task SaveAsync_MultiplePrimaryWriters_WritesToCorrectFile()
  268. {
  269. // Arrange
  270. var path1 = Path.Combine(_testDir, "config1.json");
  271. var path2 = Path.Combine(_testDir, "config2.json");
  272. File.WriteAllText(path1, """{"Key": "Value1"}""");
  273. File.WriteAllText(path2, """{"Key": "Value2"}""");
  274. using var cfg = new CfgBuilder()
  275. .AddJson(path1, level: 0, writeable: true, isPrimaryWriter: true)
  276. .AddJson(path2, level: 0, writeable: true, isPrimaryWriter: true)
  277. .Build();
  278. // Act
  279. cfg.Set("NewKey", "NewValue");
  280. await cfg.SaveAsync();
  281. // Assert - 验证写入到了正确的文件(最后一个 PrimaryWriter)
  282. var content2 = File.ReadAllText(path2);
  283. Assert.Contains("NewKey", content2);
  284. }
  285. #endregion
  286. #region CancellationToken 测试
  287. [Fact]
  288. public async Task SaveAsync_WithCancelledToken_ThrowsCancellationException()
  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: true, isPrimaryWriter: true)
  295. .Build();
  296. cfg.Set("NewKey", "NewValue");
  297. var cts = new CancellationTokenSource();
  298. cts.Cancel();
  299. // Act & Assert - TaskCanceledException 继承自 OperationCanceledException
  300. var ex = await Assert.ThrowsAnyAsync<OperationCanceledException>(
  301. async () => await cfg.SaveAsync(cancellationToken: cts.Token));
  302. Assert.True(ex is OperationCanceledException || ex is TaskCanceledException);
  303. }
  304. #endregion
  305. #region 环境变量异常测试
  306. [Fact]
  307. public void AddEnvironmentVariables_WithInvalidPrefix_Works()
  308. {
  309. // Arrange
  310. var jsonPath = Path.Combine(_testDir, "config.json");
  311. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  312. // Act - 使用不存在的前缀不应抛出异常
  313. using var cfg = new CfgBuilder()
  314. .AddJson(jsonPath, level: 0, writeable: false)
  315. .AddEnvironmentVariables(level: 1, prefix: "NONEXISTENT_PREFIX_12345_")
  316. .Build();
  317. // Assert
  318. Assert.Equal("Value", cfg.Get("Key"));
  319. }
  320. #endregion
  321. }