using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; namespace Apq.Cfg.Tests; /// /// 依赖注入扩展测试 /// public class ServiceCollectionExtensionsTests : IDisposable { private readonly string _testDir; public ServiceCollectionExtensionsTests() { _testDir = Path.Combine(Path.GetTempPath(), $"ApqCfgTests_{Guid.NewGuid():N}"); Directory.CreateDirectory(_testDir); } public void Dispose() { if (Directory.Exists(_testDir)) { Directory.Delete(_testDir, true); } } #region AddApqCfg 基本功能 [Fact] public void AddApqCfg_RegistersICfgRoot() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """{"AppName": "TestApp"}"""); var services = new ServiceCollection(); // Act services.AddApqCfg(cfg => cfg .AddJson(jsonPath, level: 0, writeable: false)); var provider = services.BuildServiceProvider(); // Assert var cfgRoot = provider.GetService(); Assert.NotNull(cfgRoot); Assert.Equal("TestApp", cfgRoot.Get("AppName")); } [Fact] public void AddApqCfg_RegistersIConfigurationRoot() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """{"AppName": "TestApp"}"""); var services = new ServiceCollection(); // Act services.AddApqCfg(cfg => cfg .AddJson(jsonPath, level: 0, writeable: false)); var provider = services.BuildServiceProvider(); // Assert var msConfig = provider.GetService(); Assert.NotNull(msConfig); Assert.Equal("TestApp", msConfig["AppName"]); } [Fact] public void AddApqCfg_RegistersAsSingleton() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """{"Key": "Value"}"""); var services = new ServiceCollection(); services.AddApqCfg(cfg => cfg .AddJson(jsonPath, level: 0, writeable: false)); var provider = services.BuildServiceProvider(); // Act var cfg1 = provider.GetService(); var cfg2 = provider.GetService(); // Assert Assert.Same(cfg1, cfg2); } #endregion #region AddApqCfg 工厂方法 [Fact] public void AddApqCfg_WithFactory_RegistersICfgRoot() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """{"FactoryTest": "Success"}"""); var services = new ServiceCollection(); // Act services.AddApqCfg(sp => { return new CfgBuilder() .AddJson(jsonPath, level: 0, writeable: false) .Build(); }); var provider = services.BuildServiceProvider(); // Assert var cfgRoot = provider.GetService(); Assert.NotNull(cfgRoot); Assert.Equal("Success", cfgRoot.Get("FactoryTest")); } #endregion #region ConfigureApqCfg 强类型配置 [Fact] public void ConfigureApqCfg_BindsOptions() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """ { "Database": { "Host": "localhost", "Port": 5432, "Name": "testdb" } } """); var services = new ServiceCollection(); services.AddApqCfg(cfg => cfg .AddJson(jsonPath, level: 0, writeable: false)); services.ConfigureApqCfg("Database"); var provider = services.BuildServiceProvider(); // Act var options = provider.GetRequiredService>().Value; // Assert Assert.Equal("localhost", options.Host); Assert.Equal(5432, options.Port); Assert.Equal("testdb", options.Name); } [Fact] public void AddApqCfg_WithOptions_BindsOptions() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """ { "App": { "Name": "MyApp", "Version": "2.0.0", "Debug": true } } """); var services = new ServiceCollection(); services.AddApqCfg( cfg => cfg.AddJson(jsonPath, level: 0, writeable: false), sectionKey: "App"); var provider = services.BuildServiceProvider(); // Act var options = provider.GetRequiredService>().Value; // Assert Assert.Equal("MyApp", options.Name); Assert.Equal("2.0.0", options.Version); Assert.True(options.Debug); } [Fact] public void ConfigureApqCfg_WithNestedSection_BindsCorrectly() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """ { "Services": { "Api": { "Url": "https://api.example.com", "Timeout": 30 } } } """); var services = new ServiceCollection(); services.AddApqCfg(cfg => cfg .AddJson(jsonPath, level: 0, writeable: false)); services.ConfigureApqCfg("Services:Api"); var provider = services.BuildServiceProvider(); // Act var options = provider.GetRequiredService>().Value; // Assert Assert.Equal("https://api.example.com", options.Url); Assert.Equal(30, options.Timeout); } #endregion #region 类型转换测试 [Fact] public void ConfigureApqCfg_ConvertsVariousTypes() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """ { "TypeTest": { "IntValue": 42, "LongValue": 9223372036854775807, "BoolValue": true, "DoubleValue": 3.14159, "DecimalValue": 123.456, "GuidValue": "550e8400-e29b-41d4-a716-446655440000", "DateTimeValue": "2024-01-15T10:30:00" } } """); var services = new ServiceCollection(); services.AddApqCfg(cfg => cfg .AddJson(jsonPath, level: 0, writeable: false)); services.ConfigureApqCfg("TypeTest"); var provider = services.BuildServiceProvider(); // Act var options = provider.GetRequiredService>().Value; // Assert Assert.Equal(42, options.IntValue); Assert.Equal(9223372036854775807L, options.LongValue); Assert.True(options.BoolValue); Assert.Equal(3.14159, options.DoubleValue, 5); Assert.Equal(123.456m, options.DecimalValue); Assert.Equal(Guid.Parse("550e8400-e29b-41d4-a716-446655440000"), options.GuidValue); Assert.Equal(new DateTime(2024, 1, 15, 10, 30, 0), options.DateTimeValue); } [Fact] public void ConfigureApqCfg_ConvertsEnumType() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """ { "Logging": { "Level": "Warning" } } """); var services = new ServiceCollection(); services.AddApqCfg(cfg => cfg .AddJson(jsonPath, level: 0, writeable: false)); services.ConfigureApqCfg("Logging"); var provider = services.BuildServiceProvider(); // Act var options = provider.GetRequiredService>().Value; // Assert Assert.Equal(LogLevel.Warning, options.Level); } #endregion #region 边界情况 [Fact] public void ConfigureApqCfg_NonExistentSection_ReturnsDefaultValues() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """{"Other": "Value"}"""); var services = new ServiceCollection(); services.AddApqCfg(cfg => cfg .AddJson(jsonPath, level: 0, writeable: false)); services.ConfigureApqCfg("NonExistent"); var provider = services.BuildServiceProvider(); // Act var options = provider.GetRequiredService>().Value; // Assert Assert.Null(options.Host); Assert.Equal(0, options.Port); Assert.Null(options.Name); } [Fact] public void AddApqCfg_CalledTwice_UsesFirstRegistration() { // Arrange var jsonPath1 = Path.Combine(_testDir, "config1.json"); var jsonPath2 = Path.Combine(_testDir, "config2.json"); File.WriteAllText(jsonPath1, """{"Source": "First"}"""); File.WriteAllText(jsonPath2, """{"Source": "Second"}"""); var services = new ServiceCollection(); // Act - 第二次调用应该被忽略(TryAddSingleton) services.AddApqCfg(cfg => cfg.AddJson(jsonPath1, level: 0, writeable: false)); services.AddApqCfg(cfg => cfg.AddJson(jsonPath2, level: 0, writeable: false)); var provider = services.BuildServiceProvider(); var cfgRoot = provider.GetRequiredService(); // Assert Assert.Equal("First", cfgRoot.Get("Source")); } #endregion #region IOptionsMonitor 测试 [Fact] public void ConfigureApqCfg_RegistersIOptionsMonitor() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """ { "Database": { "Host": "localhost", "Port": 5432 } } """); var services = new ServiceCollection(); services.AddApqCfg(cfg => cfg .AddJson(jsonPath, level: 0, writeable: true, isPrimaryWriter: true, reloadOnChange: true)); services.ConfigureApqCfg("Database"); var provider = services.BuildServiceProvider(); // Act var monitor = provider.GetService>(); // Assert Assert.NotNull(monitor); Assert.Equal("localhost", monitor.CurrentValue.Host); Assert.Equal(5432, monitor.CurrentValue.Port); } [Fact] public async Task IOptionsMonitor_NotifiesOnChange() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """ { "Database": { "Host": "localhost", "Port": 5432 } } """); var services = new ServiceCollection(); services.AddApqCfg(cfg => cfg .AddJson(jsonPath, level: 0, writeable: true, isPrimaryWriter: true, reloadOnChange: true)); services.ConfigureApqCfg("Database"); var provider = services.BuildServiceProvider(); var monitor = provider.GetRequiredService>(); var cfgRoot = provider.GetRequiredService(); var changeNotified = false; string? newHost = null; monitor.OnChange((options, _) => { changeNotified = true; newHost = options.Host; }); // Act - 直接修改文件触发变更(而不是通过 Set 方法) // 因为 ConfigChanges 是通过文件监视器触发的 await Task.Delay(100); // 等待文件监视器初始化 File.WriteAllText(jsonPath, """ { "Database": { "Host": "newhost.local", "Port": 5432 } } """); // 等待变更通知(文件监视器 + 防抖) await Task.Delay(500); // Assert Assert.True(changeNotified); Assert.Equal("newhost.local", newHost); Assert.Equal("newhost.local", monitor.CurrentValue.Host); } #endregion #region IOptionsSnapshot 测试 [Fact] public void ConfigureApqCfg_RegistersIOptionsSnapshot() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """ { "Database": { "Host": "localhost", "Port": 5432 } } """); var services = new ServiceCollection(); services.AddApqCfg(cfg => cfg .AddJson(jsonPath, level: 0, writeable: false)); services.ConfigureApqCfg("Database"); var provider = services.BuildServiceProvider(); // Act - IOptionsSnapshot 是 Scoped,需要创建 scope using var scope = provider.CreateScope(); var snapshot = scope.ServiceProvider.GetService>(); // Assert Assert.NotNull(snapshot); Assert.Equal("localhost", snapshot.Value.Host); Assert.Equal(5432, snapshot.Value.Port); } #endregion #region 嵌套对象绑定测试 [Fact] public void ConfigureApqCfg_BindsNestedObjects() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """ { "App": { "Name": "TestApp", "Database": { "Host": "db.local", "Port": 3306 }, "Cache": { "Host": "redis.local", "Port": 6379 } } } """); var services = new ServiceCollection(); services.AddApqCfg(cfg => cfg .AddJson(jsonPath, level: 0, writeable: false)); services.ConfigureApqCfg("App"); var provider = services.BuildServiceProvider(); // Act var options = provider.GetRequiredService>().Value; // Assert Assert.Equal("TestApp", options.Name); Assert.NotNull(options.Database); Assert.Equal("db.local", options.Database.Host); Assert.Equal(3306, options.Database.Port); Assert.NotNull(options.Cache); Assert.Equal("redis.local", options.Cache.Host); Assert.Equal(6379, options.Cache.Port); } [Fact] public void ConfigureApqCfg_BindsDeeplyNestedObjects() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """ { "Root": { "Level1": { "Level2": { "Value": "DeepValue" } } } } """); var services = new ServiceCollection(); services.AddApqCfg(cfg => cfg .AddJson(jsonPath, level: 0, writeable: false)); services.ConfigureApqCfg("Root"); var provider = services.BuildServiceProvider(); // Act var options = provider.GetRequiredService>().Value; // Assert Assert.NotNull(options.Level1); Assert.NotNull(options.Level1.Level2); Assert.Equal("DeepValue", options.Level1.Level2.Value); } #endregion #region 集合绑定测试 [Fact] public void ConfigureApqCfg_BindsStringArray() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """ { "Config": { "Tags": { "0": "tag1", "1": "tag2", "2": "tag3" } } } """); var services = new ServiceCollection(); services.AddApqCfg(cfg => cfg .AddJson(jsonPath, level: 0, writeable: false)); services.ConfigureApqCfg("Config"); var provider = services.BuildServiceProvider(); // Act var options = provider.GetRequiredService>().Value; // Assert Assert.NotNull(options.Tags); Assert.Collection(options.Tags, item => Assert.Equal("tag1", item), item => Assert.Equal("tag2", item), item => Assert.Equal("tag3", item)); } [Fact] public void ConfigureApqCfg_BindsIntList() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """ { "Config": { "Ports": { "0": 80, "1": 443, "2": 8080 } } } """); var services = new ServiceCollection(); services.AddApqCfg(cfg => cfg .AddJson(jsonPath, level: 0, writeable: false)); services.ConfigureApqCfg("Config"); var provider = services.BuildServiceProvider(); // Act var options = provider.GetRequiredService>().Value; // Assert Assert.NotNull(options.Ports); Assert.Collection(options.Ports, item => Assert.Equal(80, item), item => Assert.Equal(443, item), item => Assert.Equal(8080, item)); } [Fact] public void ConfigureApqCfg_BindsDictionary() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """ { "Config": { "Settings": { "Key1": "Value1", "Key2": "Value2" } } } """); var services = new ServiceCollection(); services.AddApqCfg(cfg => cfg .AddJson(jsonPath, level: 0, writeable: false)); services.ConfigureApqCfg("Config"); var provider = services.BuildServiceProvider(); // Act var options = provider.GetRequiredService>().Value; // Assert Assert.NotNull(options.Settings); Assert.Collection(options.Settings.OrderBy(x => x.Key), item => { Assert.Equal("Key1", item.Key); Assert.Equal("Value1", item.Value); }, item => { Assert.Equal("Key2", item.Key); Assert.Equal("Value2", item.Value); }); } [Fact] public void ConfigureApqCfg_BindsComplexObjectList() { // Arrange var jsonPath = Path.Combine(_testDir, "config.json"); File.WriteAllText(jsonPath, """ { "Config": { "Endpoints": { "0": { "Host": "api1.local", "Port": 8001 }, "1": { "Host": "api2.local", "Port": 8002 } } } } """); var services = new ServiceCollection(); services.AddApqCfg(cfg => cfg .AddJson(jsonPath, level: 0, writeable: false)); services.ConfigureApqCfg("Config"); var provider = services.BuildServiceProvider(); // Act var options = provider.GetRequiredService>().Value; // Assert Assert.NotNull(options.Endpoints); Assert.Collection(options.Endpoints, item => { Assert.Equal("api1.local", item.Host); Assert.Equal(8001, item.Port); }, item => { Assert.Equal("api2.local", item.Host); Assert.Equal(8002, item.Port); }); } #endregion #region ConfigureApqCfg 带变更回调测试 [Fact] public async Task ConfigureApqCfg_WithOnChange_InvokesCallback() { // Arrange var jsonPath = Path.Combine(_testDir, "config-callback.json"); File.WriteAllText(jsonPath, """ { "Database": { "Host": "localhost", "Port": 5432 } } """); var services = new ServiceCollection(); services.AddApqCfg(cfg => cfg .AddJson(jsonPath, level: 0, writeable: true, isPrimaryWriter: true, reloadOnChange: true)); var callbackInvoked = false; string? newHost = null; services.ConfigureApqCfg("Database", options => { callbackInvoked = true; newHost = options.Host; }); var provider = services.BuildServiceProvider(); // 触发 IOptionsMonitor 的创建和回调注册 var monitor = provider.GetRequiredService>(); // 解析 IDisposable 服务以确保回调被注册 var disposables = provider.GetServices().ToList(); // Act - 直接修改文件触发变更 await Task.Delay(200); // 等待文件监视器初始化 File.WriteAllText(jsonPath, """ { "Database": { "Host": "callback.local", "Port": 5432 } } """); // 等待变更通知(文件监视器 + 防抖 + 处理) await Task.Delay(800); // Assert Assert.True(callbackInvoked); Assert.Equal("callback.local", newHost); } #endregion #region 测试用选项类 public class DatabaseOptions { public string? Host { get; set; } public int Port { get; set; } public string? Name { get; set; } } public class AppOptions { public string? Name { get; set; } public string? Version { get; set; } public bool Debug { get; set; } } public class ApiOptions { public string? Url { get; set; } public int Timeout { get; set; } } public class TypeTestOptions { public int IntValue { get; set; } public long LongValue { get; set; } public bool BoolValue { get; set; } public double DoubleValue { get; set; } public decimal DecimalValue { get; set; } public Guid GuidValue { get; set; } public DateTime DateTimeValue { get; set; } } public enum LogLevel { Debug, Info, Warning, Error } public class LoggingOptions { public LogLevel Level { get; set; } } // 嵌套对象测试用类 public class AppWithNestedOptions { public string? Name { get; set; } public DatabaseOptions? Database { get; set; } public CacheOptions? Cache { get; set; } } public class CacheOptions { public string? Host { get; set; } public int Port { get; set; } } public class DeepNestedOptions { public Level1Options? Level1 { get; set; } } public class Level1Options { public Level2Options? Level2 { get; set; } } public class Level2Options { public string? Value { get; set; } } // 集合测试用类 public class ArrayOptions { public string[]? Tags { get; set; } } public class ListOptions { public List? Ports { get; set; } } public class DictionaryOptions { public Dictionary? Settings { get; set; } } public class ComplexListOptions { public List? Endpoints { get; set; } } public class EndpointOptions { public string? Host { get; set; } public int Port { get; set; } } #endregion }