| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402 |
- namespace Apq.Cfg.Tests;
- /// <summary>
- /// 异常场景测试
- /// </summary>
- public class ExceptionHandlingTests : IDisposable
- {
- private readonly string _testDir;
- public ExceptionHandlingTests()
- {
- _testDir = Path.Combine(Path.GetTempPath(), $"ApqCfgExceptionTests_{Guid.NewGuid():N}");
- Directory.CreateDirectory(_testDir);
- }
- public void Dispose()
- {
- if (Directory.Exists(_testDir))
- {
- try { Directory.Delete(_testDir, true); }
- catch { }
- }
- }
- #region 无效 JSON 测试
- [Fact]
- public void Build_InvalidJson_ThrowsException()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "invalid.json");
- File.WriteAllText(jsonPath, "{ invalid json }");
- // Act & Assert
- Assert.ThrowsAny<Exception>(() =>
- {
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false, optional: false)
- .Build();
- });
- }
- [Fact]
- public void Build_TruncatedJson_ThrowsException()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "truncated.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"""); // 缺少结束括号
- // Act & Assert
- Assert.ThrowsAny<Exception>(() =>
- {
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false, optional: false)
- .Build();
- });
- }
- #endregion
- #region 无可写源测试
- [Fact]
- public void Set_NoWritableSource_ThrowsInvalidOperationException()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "readonly.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"}""");
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false)
- .Build();
- // Act & Assert
- var ex = Assert.Throws<InvalidOperationException>(() => cfg.Set("NewKey", "NewValue"));
- Assert.Contains("没有可写的配置源", ex.Message);
- }
- [Fact]
- public void Remove_NoWritableSource_ThrowsInvalidOperationException()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "readonly.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"}""");
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false)
- .Build();
- // Act & Assert
- var ex = Assert.Throws<InvalidOperationException>(() => cfg.Remove("Key"));
- Assert.Contains("没有可写的配置源", ex.Message);
- }
- [Fact]
- public void Set_InvalidTargetLevel_ThrowsInvalidOperationException()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"}""");
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: true, isPrimaryWriter: true)
- .Build();
- // Act & Assert - 指定不存在的层级
- var ex = Assert.Throws<InvalidOperationException>(() => cfg.Set("Key", "Value", targetLevel: 999));
- Assert.Contains("没有可写的配置源", ex.Message);
- }
- [Fact]
- public void Remove_InvalidTargetLevel_ThrowsInvalidOperationException()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"}""");
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: true, isPrimaryWriter: true)
- .Build();
- // Act & Assert
- var ex = Assert.Throws<InvalidOperationException>(() => cfg.Remove("Key", targetLevel: 999));
- Assert.Contains("没有可写的配置源", ex.Message);
- }
- #endregion
- #region 类型转换异常测试
- [Fact]
- public void Get_InvalidIntConversion_ThrowsException()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- File.WriteAllText(jsonPath, """{"NotANumber": "abc"}""");
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false)
- .Build();
- // Act & Assert
- Assert.Throws<InvalidOperationException>(() => cfg.Get<int>("NotANumber"));
- }
- [Fact]
- public void Get_InvalidBoolConversion_ThrowsException()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- File.WriteAllText(jsonPath, """{"NotABool": "maybe"}""");
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false)
- .Build();
- // Act & Assert - Microsoft.Extensions.Configuration 对无效 bool 值抛出异常
- Assert.Throws<InvalidOperationException>(() => cfg.Get<bool>("NotABool"));
- }
- [Fact]
- public void Get_OverflowInt_ThrowsException()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- // 使用字符串形式的大数字
- File.WriteAllText(jsonPath, """{"TooBig": "99999999999999999999"}""");
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false)
- .Build();
- // Act & Assert - 溢出时抛出异常
- Assert.Throws<InvalidOperationException>(() => cfg.Get<int>("TooBig"));
- }
- [Fact]
- public void GetRequired_NonExistentKey_ThrowsInvalidOperationException()
- {
- // 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 & Assert
- var ex = Assert.Throws<InvalidOperationException>(() => cfg.GetRequired<string>("NonExistent"));
- Assert.Contains("NonExistent", ex.Message);
- Assert.Contains("必需的配置键", ex.Message);
- }
- #endregion
- #region 文件权限测试
- [Fact]
- public async Task SaveAsync_ReadOnlyFile_ThrowsException()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "readonly.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"}""");
- // 设置文件为只读
- var fileInfo = new FileInfo(jsonPath);
- fileInfo.IsReadOnly = true;
- try
- {
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: true, isPrimaryWriter: true)
- .Build();
- cfg.Set("NewKey", "NewValue");
- // Act & Assert - 保存到只读文件应该抛出异常
- var exception = await Assert.ThrowsAnyAsync<Exception>(async () => await cfg.SaveAsync());
- // 验证是 IO 相关的异常
- Assert.True(exception is IOException || exception is UnauthorizedAccessException,
- $"Expected IOException or UnauthorizedAccessException, but got {exception.GetType().Name}");
- }
- finally
- {
- // 清理:移除只读属性
- fileInfo.IsReadOnly = false;
- }
- }
- #endregion
- #region Dispose 后操作测试
- [Fact]
- public void Get_AfterDispose_MayThrowOrReturnNull()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"}""");
- var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false)
- .Build();
- cfg.Dispose();
- // Act & Assert - Dispose 后的行为取决于实现
- // 可能抛出 ObjectDisposedException 或返回 null
- try
- {
- var value = cfg.Get("Key");
- // 如果没有抛出异常,值可能是 null 或原值
- }
- catch (ObjectDisposedException)
- {
- // 预期的异常
- }
- }
- [Fact]
- public async Task SaveAsync_AfterDispose_MayThrowOrDoNothing()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"}""");
- var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: true, isPrimaryWriter: true)
- .Build();
- cfg.Set("NewKey", "NewValue");
- cfg.Dispose();
- // Act & Assert
- try
- {
- await cfg.SaveAsync();
- // 如果没有抛出异常,操作可能被忽略
- }
- catch (ObjectDisposedException)
- {
- // 预期的异常
- }
- catch (InvalidOperationException)
- {
- // 也可能抛出此异常
- }
- }
- #endregion
- #region 空配置源测试
- [Fact]
- public void Build_NoSources_ReturnsEmptyConfig()
- {
- // Act - 没有添加任何配置源,应该返回空配置而不是抛出异常
- using var cfg = new CfgBuilder().Build();
- // Assert - 空配置应该正常工作
- Assert.Null(cfg.Get("AnyKey"));
- Assert.False(cfg.Exists("AnyKey"));
- }
- #endregion
- #region 多 PrimaryWriter 测试
- [Fact]
- public void Build_MultiplePrimaryWriters_SameLevelUsesLast()
- {
- // Arrange
- var path1 = Path.Combine(_testDir, "config1.json");
- var path2 = Path.Combine(_testDir, "config2.json");
- File.WriteAllText(path1, """{"Key": "Value1"}""");
- File.WriteAllText(path2, """{"Key": "Value2"}""");
- // Act - 同一层级多个 PrimaryWriter
- using var cfg = new CfgBuilder()
- .AddJson(path1, level: 0, writeable: true, isPrimaryWriter: true)
- .AddJson(path2, level: 0, writeable: true, isPrimaryWriter: true)
- .Build();
- // Assert - 应该使用最后一个
- cfg.Set("NewKey", "NewValue");
- Assert.Equal("NewValue", cfg.Get("NewKey"));
- }
- [Fact]
- public async Task SaveAsync_MultiplePrimaryWriters_WritesToCorrectFile()
- {
- // Arrange
- var path1 = Path.Combine(_testDir, "config1.json");
- var path2 = Path.Combine(_testDir, "config2.json");
- File.WriteAllText(path1, """{"Key": "Value1"}""");
- File.WriteAllText(path2, """{"Key": "Value2"}""");
- using var cfg = new CfgBuilder()
- .AddJson(path1, level: 0, writeable: true, isPrimaryWriter: true)
- .AddJson(path2, level: 0, writeable: true, isPrimaryWriter: true)
- .Build();
- // Act
- cfg.Set("NewKey", "NewValue");
- await cfg.SaveAsync();
- // Assert - 验证写入到了正确的文件(最后一个 PrimaryWriter)
- var content2 = File.ReadAllText(path2);
- Assert.Contains("NewKey", content2);
- }
- #endregion
- #region CancellationToken 测试
- [Fact]
- public async Task SaveAsync_WithCancelledToken_ThrowsCancellationException()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"}""");
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: true, isPrimaryWriter: true)
- .Build();
- cfg.Set("NewKey", "NewValue");
- var cts = new CancellationTokenSource();
- cts.Cancel();
- // Act & Assert - TaskCanceledException 继承自 OperationCanceledException
- var ex = await Assert.ThrowsAnyAsync<OperationCanceledException>(
- async () => await cfg.SaveAsync(cancellationToken: cts.Token));
- Assert.True(ex is OperationCanceledException || ex is TaskCanceledException);
- }
- #endregion
- #region 环境变量异常测试
- [Fact]
- public void AddEnvironmentVariables_WithInvalidPrefix_Works()
- {
- // Arrange
- var jsonPath = Path.Combine(_testDir, "config.json");
- File.WriteAllText(jsonPath, """{"Key": "Value"}""");
- // Act - 使用不存在的前缀不应抛出异常
- using var cfg = new CfgBuilder()
- .AddJson(jsonPath, level: 0, writeable: false)
- .AddEnvironmentVariables(level: 1, prefix: "NONEXISTENT_PREFIX_12345_")
- .Build();
- // Assert
- Assert.Equal("Value", cfg.Get("Key"));
- }
- #endregion
- }
|