using System.Collections; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; namespace ZB.MOM.WW.LocalDb.Tests; public sealed class DependencyInjectionTests : IDisposable { private readonly string _path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".db"); public void Dispose() { Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools(); if (File.Exists(_path)) File.Delete(_path); } private IConfiguration Config() => new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["LocalDb:Path"] = _path }) .Build(); [Fact] public async Task AddZbLocalDb_ResolvesILocalDb_Singleton() { var services = new ServiceCollection(); services.AddZbLocalDb(Config()); await using var provider = services.BuildServiceProvider(); var a = provider.GetRequiredService(); var b = provider.GetRequiredService(); Assert.Same(a, b); await a.ExecuteAsync("CREATE TABLE t (id INTEGER PRIMARY KEY)"); await a.ExecuteAsync("INSERT INTO t (id) VALUES (@id)", new { id = 1 }); var rows = await a.QueryAsync("SELECT COUNT(*) FROM t", r => r.GetInt64(0)); Assert.Equal(1L, rows[0]); } [Fact] public void AddZbLocalDb_WithRegistrations_TablesRegisteredOnStart() { var services = new ServiceCollection(); services.AddZbLocalDb(Config(), db => { using var conn = db.CreateConnection(); using var cmd = conn.CreateCommand(); cmd.CommandText = "CREATE TABLE widget (id INTEGER PRIMARY KEY, name TEXT)"; cmd.ExecuteNonQuery(); db.RegisterReplicated("widget"); }); using var provider = services.BuildServiceProvider(); var db = provider.GetRequiredService(); Assert.Contains("widget", db.ReplicatedTables.Keys); } [Fact] public void AddZbLocalDb_OnReadyThrows_ExceptionPropagates() { var services = new ServiceCollection(); services.AddZbLocalDb(Config(), _ => throw new InvalidOperationException("boom")); using var provider = services.BuildServiceProvider(); var first = Assert.Throws(() => provider.GetRequiredService()); Assert.Equal("boom", first.Message); // No half-initialized singleton is cached: the next resolution re-runs the factory and fails again. var second = Assert.Throws(() => provider.GetRequiredService()); Assert.Equal("boom", second.Message); } [Fact] public void Validator_EmptyPath_Fails() { var result = new LocalDbOptionsValidator().Validate( null, new LocalDbOptions { Path = " " }); Assert.True(result.Failed); Assert.Contains(result.Failures, f => f.Contains("LocalDb:Path", StringComparison.Ordinal)); } [Fact] public void Validator_NonPositiveBusyTimeout_Fails() { var result = new LocalDbOptionsValidator().Validate( null, new LocalDbOptions { Path = "x.db", BusyTimeoutMs = 0 }); Assert.True(result.Failed); Assert.Contains(result.Failures, f => f.Contains("BusyTimeoutMs", StringComparison.Ordinal)); } [Fact] public void Validator_InvalidSynchronous_Fails() { var result = new LocalDbOptionsValidator().Validate( null, new LocalDbOptions { Path = "x.db", Synchronous = "SOMETIMES" }); Assert.True(result.Failed); Assert.Contains(result.Failures, f => f.Contains("Synchronous", StringComparison.Ordinal)); } [Fact] public void Validator_ValidOptions_Succeeds() { var result = new LocalDbOptionsValidator().Validate( null, new LocalDbOptions { Path = "x.db", BusyTimeoutMs = 5000, Synchronous = "normal" }); Assert.True(result.Succeeded); } [Fact] public async Task Bind_NonObjectDictionary_Throws() { var services = new ServiceCollection(); services.AddZbLocalDb(Config()); await using var provider = services.BuildServiceProvider(); var db = provider.GetRequiredService(); await db.ExecuteAsync("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)"); var stringDict = new Dictionary { ["id"] = "1" }; await Assert.ThrowsAsync(() => db.ExecuteAsync("INSERT INTO t (id) VALUES (@id)", stringDict)); var hashtable = new Hashtable { ["id"] = 1 }; await Assert.ThrowsAsync(() => db.ExecuteAsync("INSERT INTO t (id) VALUES (@id)", hashtable)); } }