using Microsoft.Data.Sqlite; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using ZB.MOM.WW.Auth.Abstractions.ApiKeys; using ZB.MOM.WW.Auth.ApiKeys.DependencyInjection; namespace ZB.MOM.WW.Auth.ApiKeys.Tests; public class ApiKeyServiceCollectionExtensionsTests { private const string ApiKeySection = "Auth:ApiKeys"; private const string PepperSecretName = "ApiKeyPepper"; private const string PepperValue = "super-secret-pepper-value"; private static IConfiguration BuildConfiguration(string sqlitePath, bool runMigrationsOnStartup = false) => new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { [$"{ApiKeySection}:TokenPrefix"] = "mxgw", [$"{ApiKeySection}:SqlitePath"] = sqlitePath, [$"{ApiKeySection}:PepperSecretName"] = PepperSecretName, [$"{ApiKeySection}:RunMigrationsOnStartup"] = runMigrationsOnStartup ? "true" : "false", // The pepper itself lives at the top level under the configured secret name. [PepperSecretName] = PepperValue, }) .Build(); private static string TempSqlitePath() => Path.Combine(Path.GetTempPath(), $"zbauth-test-{Guid.NewGuid():N}.db"); [Fact] public void AddZbApiKeyAuth_ResolvesVerifier() { IConfiguration config = BuildConfiguration(TempSqlitePath()); var services = new ServiceCollection(); services.AddZbApiKeyAuth(config, ApiKeySection); using ServiceProvider provider = services.BuildServiceProvider(); var verifier = provider.GetRequiredService(); Assert.NotNull(verifier); } [Fact] public void AddZbApiKeyAuth_ResolvesAllStores() { IConfiguration config = BuildConfiguration(TempSqlitePath()); var services = new ServiceCollection(); services.AddZbApiKeyAuth(config, ApiKeySection); using ServiceProvider provider = services.BuildServiceProvider(); Assert.NotNull(provider.GetRequiredService()); Assert.NotNull(provider.GetRequiredService()); Assert.NotNull(provider.GetRequiredService()); } [Fact] public void AddZbApiKeyAuth_BindsOptionsFromSection() { string sqlitePath = TempSqlitePath(); IConfiguration config = BuildConfiguration(sqlitePath); var services = new ServiceCollection(); services.AddZbApiKeyAuth(config, ApiKeySection); using ServiceProvider provider = services.BuildServiceProvider(); var options = provider.GetRequiredService>(); Assert.Equal("mxgw", options.Value.TokenPrefix); Assert.Equal(sqlitePath, options.Value.SqlitePath); Assert.Equal(PepperSecretName, options.Value.PepperSecretName); } [Fact] public void AddZbApiKeyAuth_PepperProviderReturnsConfiguredPepper() { IConfiguration config = BuildConfiguration(TempSqlitePath()); var services = new ServiceCollection(); services.AddZbApiKeyAuth(config, ApiKeySection); using ServiceProvider provider = services.BuildServiceProvider(); var pepperProvider = provider.GetRequiredService(); Assert.IsType(pepperProvider); Assert.Equal(PepperValue, pepperProvider.GetPepper()); } [Fact] public void ConfigurationApiKeyPepperProvider_ReturnsNull_WhenSecretNameUnset() { IConfiguration config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary()) .Build(); var options = Options.Create(new ApiKeyOptions { PepperSecretName = "" }); var provider = new ConfigurationApiKeyPepperProvider(config, options); Assert.Null(provider.GetPepper()); } [Fact] public void ConfigurationApiKeyPepperProvider_ReturnsNull_WhenValueAbsent() { IConfiguration config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary()) .Build(); var options = Options.Create(new ApiKeyOptions { PepperSecretName = "Missing" }); var provider = new ConfigurationApiKeyPepperProvider(config, options); Assert.Null(provider.GetPepper()); } [Fact] public async Task AddZbApiKeyAuth_MigrationHostedService_CreatesSchemaOnStartup() { string sqlitePath = TempSqlitePath(); try { IConfiguration config = BuildConfiguration(sqlitePath, runMigrationsOnStartup: true); var services = new ServiceCollection(); services.AddZbApiKeyAuth(config, ApiKeySection); await using ServiceProvider provider = services.BuildServiceProvider(); // Find the ApiKeyMigrationHostedService among all registered IHostedService instances. var hostedServices = provider.GetServices().ToList(); IHostedService? migrationService = hostedServices .FirstOrDefault(s => s.GetType().Name == "ApiKeyMigrationHostedService"); Assert.NotNull(migrationService); await migrationService!.StartAsync(CancellationToken.None); // Verify the api_keys table was created by the migration. string connectionString = new SqliteConnectionStringBuilder { DataSource = sqlitePath, Mode = SqliteOpenMode.ReadOnly, }.ToString(); await using var connection = new SqliteConnection(connectionString); await connection.OpenAsync(); await using var command = connection.CreateCommand(); command.CommandText = """ SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'api_keys'; """; long tableCount = (long)(await command.ExecuteScalarAsync() ?? 0L); Assert.Equal(1L, tableCount); } finally { if (File.Exists(sqlitePath)) File.Delete(sqlitePath); } } [Fact] public async Task AddZbApiKeyAuth_MigrationHostedService_SkipsMigration_WhenRunMigrationsOnStartupFalse() { string sqlitePath = TempSqlitePath(); try { IConfiguration config = BuildConfiguration(sqlitePath, runMigrationsOnStartup: false); var services = new ServiceCollection(); services.AddZbApiKeyAuth(config, ApiKeySection); await using ServiceProvider provider = services.BuildServiceProvider(); var hostedServices = provider.GetServices().ToList(); IHostedService? migrationService = hostedServices .FirstOrDefault(s => s.GetType().Name == "ApiKeyMigrationHostedService"); Assert.NotNull(migrationService); // StartAsync should complete without creating the database file. await migrationService!.StartAsync(CancellationToken.None); Assert.False(File.Exists(sqlitePath), "Migration should not run when RunMigrationsOnStartup is false."); } finally { if (File.Exists(sqlitePath)) File.Delete(sqlitePath); } } }