diff --git a/samples/KeyVault/appsettings.Production.json b/samples/KeyVault/appsettings.Production.json index e000fe56..4b50e54d 100644 --- a/samples/KeyVault/appsettings.Production.json +++ b/samples/KeyVault/appsettings.Production.json @@ -1,9 +1,11 @@ { "LettuceEncrypt": { "DomainNames": [ - "example.com", - "www.example.com", - "www2.example.com" + [ + "example.com", + "www.example.com", + "www2.example.com" + ] ], "AzureKeyVault": { "AzureKeyVaultEndpoint": "https://my-production-secrets.vault.azure.net" diff --git a/samples/KeyVault/appsettings.Staging.json b/samples/KeyVault/appsettings.Staging.json index 8170802c..c69b3539 100644 --- a/samples/KeyVault/appsettings.Staging.json +++ b/samples/KeyVault/appsettings.Staging.json @@ -1,7 +1,7 @@ { "LettuceEncrypt": { "DomainNames": [ - "staging.example.com" + [ "staging.example.com" ] ], "AzureKeyVault": { "AzureKeyVaultEndpoint": "https://my-staging-secrets.vault.azure.net" diff --git a/samples/Web/appsettings.Production.json b/samples/Web/appsettings.Production.json index 7d6c2e19..9e20ac10 100644 --- a/samples/Web/appsettings.Production.json +++ b/samples/Web/appsettings.Production.json @@ -1,9 +1,11 @@ { "LettuceEncrypt": { "DomainNames": [ - "example.com", - "www.example.com", - "www2.example.com" + [ + "example.com", + "www.example.com", + "www2.example.com" + ] ] } } diff --git a/samples/Web/appsettings.Staging.json b/samples/Web/appsettings.Staging.json index 427f1938..57104080 100644 --- a/samples/Web/appsettings.Staging.json +++ b/samples/Web/appsettings.Staging.json @@ -1,7 +1,7 @@ { "LettuceEncrypt": { "DomainNames": [ - "staging.example.com" + [ "staging.example.com" ] ], "UseStagingServer": true } diff --git a/src/LettuceEncrypt.Azure/Internal/AzureKeyVaultCertificateRepository.cs b/src/LettuceEncrypt.Azure/Internal/AzureKeyVaultCertificateRepository.cs index 063ca98c..2cbeb341 100644 --- a/src/LettuceEncrypt.Azure/Internal/AzureKeyVaultCertificateRepository.cs +++ b/src/LettuceEncrypt.Azure/Internal/AzureKeyVaultCertificateRepository.cs @@ -34,7 +34,7 @@ public async Task> GetCertificatesAsync(Cancellati { var certs = new List(); - foreach (var domain in _encryptOptions.Value.DomainNames) + foreach (var domain in _encryptOptions.Value.DomainNames.SelectMany(domainName => domainName)) { var cert = await GetCertificateWithPrivateKeyAsync(domain, cancellationToken); diff --git a/src/LettuceEncrypt/Internal/AcmeCertificateFactory.cs b/src/LettuceEncrypt/Internal/AcmeCertificateFactory.cs index a7543965..72e2ffd0 100644 --- a/src/LettuceEncrypt/Internal/AcmeCertificateFactory.cs +++ b/src/LettuceEncrypt/Internal/AcmeCertificateFactory.cs @@ -153,7 +153,7 @@ private async Task ExistingAccountIsValidAsync() return true; } - public async Task CreateCertificateAsync(CancellationToken cancellationToken) + public async Task CreateCertificateAsync(string[] domainNames, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); if (_client == null) @@ -165,7 +165,7 @@ public async Task CreateCertificateAsync(CancellationToken can var orders = await _client.GetOrdersAsync(); if (orders.Any()) { - var expectedDomains = new HashSet(_options.Value.DomainNames); + var expectedDomains = new HashSet(domainNames); foreach (var order in orders) { var orderDetails = await _client.GetOrderDetailsAsync(order); @@ -191,7 +191,7 @@ public async Task CreateCertificateAsync(CancellationToken can if (orderContext == null) { _logger.LogDebug("Creating new order for a certificate"); - orderContext = await _client.CreateOrderAsync(_options.Value.DomainNames); + orderContext = await _client.CreateOrderAsync(domainNames); } cancellationToken.ThrowIfCancellationRequested(); @@ -201,7 +201,7 @@ public async Task CreateCertificateAsync(CancellationToken can await Task.WhenAll(BeginValidateAllAuthorizations(authorizations, cancellationToken)); cancellationToken.ThrowIfCancellationRequested(); - return await CompleteCertificateRequestAsync(orderContext, cancellationToken); + return await CompleteCertificateRequestAsync(orderContext, domainNames, cancellationToken); } private IEnumerable BeginValidateAllAuthorizations(IEnumerable authorizations, @@ -282,7 +282,7 @@ private async Task ValidateDomainOwnershipAsync(IAuthorizationContext authorizat throw new InvalidOperationException($"Failed to validate ownership of domainName '{domainName}'"); } - private async Task CompleteCertificateRequestAsync(IOrderContext order, + private async Task CompleteCertificateRequestAsync(IOrderContext order, string[] domainNames, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -291,7 +291,7 @@ private async Task CompleteCertificateRequestAsync(IOrderConte throw new InvalidOperationException(); } - var commonName = _options.Value.DomainNames[0]; + var commonName = domainNames[0]; _logger.LogDebug("Creating cert for {commonName}", commonName); var csrInfo = new CsrInfo @@ -305,7 +305,7 @@ private async Task CompleteCertificateRequestAsync(IOrderConte _logger.LogAcmeAction("NewCertificate"); var pfxBuilder = CreatePfxBuilder(acmeCert, privateKey); - var pfx = pfxBuilder.Build("HTTPS Cert - " + _options.Value.DomainNames, string.Empty); + var pfx = pfxBuilder.Build("HTTPS Cert - " + domainNames, string.Empty); return new X509Certificate2(pfx, string.Empty, X509KeyStorageFlags.Exportable); } diff --git a/src/LettuceEncrypt/Internal/AcmeCertificateLoader.cs b/src/LettuceEncrypt/Internal/AcmeCertificateLoader.cs index d13b58b4..c5a2b428 100644 --- a/src/LettuceEncrypt/Internal/AcmeCertificateLoader.cs +++ b/src/LettuceEncrypt/Internal/AcmeCertificateLoader.cs @@ -91,6 +91,8 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } private bool LettuceEncryptDomainNamesWereConfigured() - => _options.Value.DomainNames - .Any(w => !string.Equals("localhost", w, StringComparison.OrdinalIgnoreCase)); + { + return _options.Value.DomainNames.All(domains => + domains.Any(w => !string.Equals("localhost", w, StringComparison.OrdinalIgnoreCase))); + } } diff --git a/src/LettuceEncrypt/Internal/AcmeStates/BeginCertificateCreationState.cs b/src/LettuceEncrypt/Internal/AcmeStates/BeginCertificateCreationState.cs index 4f73f73f..feb487f8 100644 --- a/src/LettuceEncrypt/Internal/AcmeStates/BeginCertificateCreationState.cs +++ b/src/LettuceEncrypt/Internal/AcmeStates/BeginCertificateCreationState.cs @@ -9,28 +9,31 @@ namespace LettuceEncrypt.Internal.AcmeStates; internal class BeginCertificateCreationState : AcmeState { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IOptions _options; private readonly AcmeCertificateFactory _acmeCertificateFactory; private readonly CertificateSelector _selector; + private readonly DomainNamesEnumerator _domainNamesEnumerator; private readonly IEnumerable _certificateRepositories; public BeginCertificateCreationState( - AcmeStateMachineContext context, ILogger logger, + AcmeStateMachineContext context, ILogger logger, IOptions options, AcmeCertificateFactory acmeCertificateFactory, - CertificateSelector selector, IEnumerable certificateRepositories) + CertificateSelector selector, DomainNamesEnumerator domainNamesEnumerator, + IEnumerable certificateRepositories) : base(context) { _logger = logger; _options = options; _acmeCertificateFactory = acmeCertificateFactory; _selector = selector; + _domainNamesEnumerator = domainNamesEnumerator; _certificateRepositories = certificateRepositories; } public override async Task MoveNextAsync(CancellationToken cancellationToken) { - var domainNames = _options.Value.DomainNames; + var domainNames = _domainNamesEnumerator.Current; try { @@ -40,7 +43,7 @@ public override async Task MoveNextAsync(CancellationToken cancellat _logger.LogInformation("Creating certificate for {hostname}", string.Join(",", domainNames)); - var cert = await _acmeCertificateFactory.CreateCertificateAsync(cancellationToken); + var cert = await _acmeCertificateFactory.CreateCertificateAsync(domainNames, cancellationToken); _logger.LogInformation("Created certificate {subjectName} ({thumbprint})", cert.Subject, @@ -50,7 +53,7 @@ public override async Task MoveNextAsync(CancellationToken cancellat } catch (Exception ex) { - _logger.LogError(0, ex, "Failed to automatically create a certificate for {hostname}", domainNames); + _logger.LogError(0, ex, "Failed to automatically create a certificate for {hostname}", string.Join(", ", domainNames)); throw; } @@ -61,17 +64,18 @@ private async Task SaveCertificateAsync(X509Certificate2 cert, CancellationToken { _selector.Add(cert); - var saveTasks = new List - { - Task.Delay(TimeSpan.FromMinutes(5), cancellationToken) - }; + var saveTasks = new List(); + + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + linkedCts.CancelAfter(TimeSpan.FromMinutes(5)); + var linkedToken = linkedCts.Token; var errors = new List(); foreach (var repo in _certificateRepositories) { try { - saveTasks.Add(repo.SaveAsync(cert, cancellationToken)); + saveTasks.Add(repo.SaveAsync(cert, linkedToken)); } catch (Exception ex) { @@ -80,7 +84,14 @@ private async Task SaveCertificateAsync(X509Certificate2 cert, CancellationToken } } - await Task.WhenAll(saveTasks); + try + { + await Task.WhenAll(saveTasks); + } + catch (TaskCanceledException e) + { + errors.Add(e); + } if (errors.Count > 0) { diff --git a/src/LettuceEncrypt/Internal/AcmeStates/CheckForRenewalState.cs b/src/LettuceEncrypt/Internal/AcmeStates/CheckForRenewalState.cs index dff6de9e..00a6ee54 100644 --- a/src/LettuceEncrypt/Internal/AcmeStates/CheckForRenewalState.cs +++ b/src/LettuceEncrypt/Internal/AcmeStates/CheckForRenewalState.cs @@ -7,11 +7,12 @@ namespace LettuceEncrypt.Internal.AcmeStates; -internal class CheckForRenewalState : AcmeState +internal class CheckForRenewalState : SyncAcmeState { private readonly ILogger _logger; private readonly IOptions _options; private readonly CertificateSelector _selector; + private readonly DomainNamesEnumerator _domainNamesEnumerator; private readonly IClock _clock; public CheckForRenewalState( @@ -19,47 +20,45 @@ public CheckForRenewalState( ILogger logger, IOptions options, CertificateSelector selector, + DomainNamesEnumerator domainNamesEnumerator, IClock clock) : base(context) { _logger = logger; _options = options; _selector = selector; + _domainNamesEnumerator = domainNamesEnumerator; _clock = clock; } - public override async Task MoveNextAsync(CancellationToken cancellationToken) + public override IAcmeState MoveNext() { - while (!cancellationToken.IsCancellationRequested) + var checkPeriod = _options.Value.RenewalCheckPeriod; + var daysInAdvance = _options.Value.RenewDaysInAdvance; + if (!checkPeriod.HasValue || !daysInAdvance.HasValue) { - var checkPeriod = _options.Value.RenewalCheckPeriod; - var daysInAdvance = _options.Value.RenewDaysInAdvance; - if (!checkPeriod.HasValue || !daysInAdvance.HasValue) - { - _logger.LogInformation("Automatic certificate renewal is not configured. Stopping {service}", - nameof(AcmeCertificateLoader)); - return MoveTo(); - } + _logger.LogInformation("Automatic certificate renewal is not configured. Stopping {service}", + nameof(AcmeCertificateLoader)); + return MoveTo(); + } - var domainNames = _options.Value.DomainNames; - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Checking certificates' renewals for {hostname}", - string.Join(", ", domainNames)); - } + var domainNames = _domainNamesEnumerator.Current; - foreach (var domainName in domainNames) + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Checking certificates' renewals for {hostname}", + string.Join(", ", domainNames)); + } + + foreach (var domainName in domainNames) + { + if (!_selector.TryGet(domainName, out var cert) + || cert == null + || cert.NotAfter <= _clock.Now.DateTime + daysInAdvance.Value) { - if (!_selector.TryGet(domainName, out var cert) - || cert == null - || cert.NotAfter <= _clock.Now.DateTime + daysInAdvance.Value) - { - return MoveTo(); - } + return MoveTo(); } - - await Task.Delay(checkPeriod.Value, cancellationToken); } - return MoveTo(); + return MoveTo(); } } diff --git a/src/LettuceEncrypt/Internal/AcmeStates/ServerStartupState.cs b/src/LettuceEncrypt/Internal/AcmeStates/ServerStartupState.cs index a4458a1e..e0a8509b 100644 --- a/src/LettuceEncrypt/Internal/AcmeStates/ServerStartupState.cs +++ b/src/LettuceEncrypt/Internal/AcmeStates/ServerStartupState.cs @@ -10,30 +10,39 @@ internal class ServerStartupState : SyncAcmeState { private readonly IOptions _options; private readonly CertificateSelector _selector; + private readonly DomainNamesEnumerator _domainNamesEnumerator; private readonly ILogger _logger; public ServerStartupState( AcmeStateMachineContext context, IOptions options, CertificateSelector selector, + DomainNamesEnumerator domainNamesEnumerator, ILogger logger) : base(context) { _options = options; _selector = selector; + _domainNamesEnumerator = domainNamesEnumerator; _logger = logger; } public override IAcmeState MoveNext() { - var domainNames = _options.Value.DomainNames; - var hasCertForAllDomains = domainNames.All(_selector.HasCertForDomain); - if (hasCertForAllDomains) + while (_domainNamesEnumerator.MoveNext()) { - _logger.LogDebug("Certificate for {domainNames} already found.", domainNames); - return MoveTo(); + var domainNames = _domainNamesEnumerator.Current; + var hasCertForAllDomains = domainNames.All(_selector.HasCertForDomain); + if (hasCertForAllDomains) + { + _logger.LogDebug("Certificate for [{domainNames}] already found.", string.Join(", ", domainNames)); + return MoveTo(); + } + + return MoveTo(); } - return MoveTo(); + _domainNamesEnumerator.Reset(); + return MoveTo(); } } diff --git a/src/LettuceEncrypt/Internal/AcmeStates/WaitState.cs b/src/LettuceEncrypt/Internal/AcmeStates/WaitState.cs new file mode 100644 index 00000000..cc7dab3e --- /dev/null +++ b/src/LettuceEncrypt/Internal/AcmeStates/WaitState.cs @@ -0,0 +1,44 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using LettuceEncrypt.Internal.IO; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace LettuceEncrypt.Internal.AcmeStates; + +internal class WaitState : AcmeState +{ + private readonly ILogger _logger; + private readonly IOptions _options; + private readonly CertificateSelector _selector; + private readonly IClock _clock; + + public WaitState( + AcmeStateMachineContext context, + ILogger logger, + IOptions options, + CertificateSelector selector, + IClock clock) : base(context) + { + _logger = logger; + _options = options; + _selector = selector; + _clock = clock; + } + + public override async Task MoveNextAsync(CancellationToken cancellationToken) + { + var checkPeriod = _options.Value.RenewalCheckPeriod; + if (!checkPeriod.HasValue) + { + _logger.LogInformation("Automatic certificate renewal is not configured. Stopping {service}", + nameof(AcmeCertificateLoader)); + return MoveTo(); + } + + await Task.Delay(checkPeriod.Value, cancellationToken); + + return MoveTo(); + } +} diff --git a/src/LettuceEncrypt/Internal/DomainNamesEnumerator.cs b/src/LettuceEncrypt/Internal/DomainNamesEnumerator.cs new file mode 100644 index 00000000..f0001c28 --- /dev/null +++ b/src/LettuceEncrypt/Internal/DomainNamesEnumerator.cs @@ -0,0 +1,53 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections; +using Microsoft.Extensions.Options; + +namespace LettuceEncrypt.Internal; + +internal class DomainNamesEnumerator : IEnumerator +{ + private readonly string[][] _domains; + + private int _position = -1; + + public DomainNamesEnumerator(IOptions options) + { + _domains = options.Value.DomainNames; + } + + public bool MoveNext() + { + _position++; + return _position < _domains.Length; + } + + public void Reset() + { + _position = -1; + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + public string[] Current + { + get + { + try + { + return _domains[_position]; + } + catch (IndexOutOfRangeException) + { + throw new InvalidOperationException(); + } + } + } +} diff --git a/src/LettuceEncrypt/Internal/OptionsValidation.cs b/src/LettuceEncrypt/Internal/OptionsValidation.cs index 71618cd6..f7c26c62 100644 --- a/src/LettuceEncrypt/Internal/OptionsValidation.cs +++ b/src/LettuceEncrypt/Internal/OptionsValidation.cs @@ -13,7 +13,7 @@ public ValidateOptionsResult Validate(string name, LettuceEncryptOptions options if (options.AllowedChallengeTypes == ChallengeType.Dns01) return ValidateOptionsResult.Success; - foreach (var dnsName in options.DomainNames) + foreach (var dnsName in options.DomainNames.SelectMany(domainName => domainName)) { if (dnsName.Contains('*')) { diff --git a/src/LettuceEncrypt/Internal/X509CertStore.cs b/src/LettuceEncrypt/Internal/X509CertStore.cs index d3e5ad0f..636e1efc 100644 --- a/src/LettuceEncrypt/Internal/X509CertStore.cs +++ b/src/LettuceEncrypt/Internal/X509CertStore.cs @@ -25,7 +25,7 @@ public X509CertStore(IOptions options, ILogger> GetCertificatesAsync(CancellationToken cancellationToken) { - var domainNames = new HashSet(_options.Value.DomainNames); + var domainNames = new HashSet(_options.Value.DomainNames.SelectMany(domainName => domainName)); var result = new List(); var certs = _store.Certificates.Find(X509FindType.FindByTimeValid, DateTime.Now, diff --git a/src/LettuceEncrypt/LettuceEncryptOptions.cs b/src/LettuceEncrypt/LettuceEncryptOptions.cs index cc9620ad..08314e2a 100644 --- a/src/LettuceEncrypt/LettuceEncryptOptions.cs +++ b/src/LettuceEncrypt/LettuceEncryptOptions.cs @@ -11,13 +11,16 @@ namespace LettuceEncrypt; /// public class LettuceEncryptOptions { - private string[] _domainNames = Array.Empty(); + private string[][] _domainNames = Array.Empty(); private bool? _useStagingServer; /// /// The domain names for which to generate certificates. + /// An array of arrays of domain names for which to generate certificates. + /// The domains in each child array share a single certificate. + /// Each child array will have its own certificate. /// - public string[] DomainNames + public string[][] DomainNames { get => _domainNames; set => _domainNames = value ?? throw new ArgumentNullException(nameof(value)); diff --git a/src/LettuceEncrypt/LettuceEncryptServiceCollectionExtensions.cs b/src/LettuceEncrypt/LettuceEncryptServiceCollectionExtensions.cs index 3beaeacf..32146e21 100644 --- a/src/LettuceEncrypt/LettuceEncryptServiceCollectionExtensions.cs +++ b/src/LettuceEncrypt/LettuceEncryptServiceCollectionExtensions.cs @@ -78,6 +78,7 @@ public static ILettuceEncryptServiceBuilder AddLettuceEncrypt(this IServiceColle // The state machine should run in its own scope services.AddScoped(); + services.AddScoped(); services.AddSingleton(TerminalState.Singleton); @@ -85,7 +86,8 @@ public static ILettuceEncryptServiceBuilder AddLettuceEncrypt(this IServiceColle services .AddTransient() .AddTransient() - .AddTransient(); + .AddTransient() + .AddTransient(); // PfxBuilderFactory is stateless, so there's no need for a transient registration services.AddSingleton(); diff --git a/src/LettuceEncrypt/PublicAPI.Shipped.txt b/src/LettuceEncrypt/PublicAPI.Shipped.txt index 6b4b2195..864310fa 100644 --- a/src/LettuceEncrypt/PublicAPI.Shipped.txt +++ b/src/LettuceEncrypt/PublicAPI.Shipped.txt @@ -52,7 +52,7 @@ LettuceEncrypt.LettuceEncryptOptions.AdditionalIssuers.get -> string![]! LettuceEncrypt.LettuceEncryptOptions.AdditionalIssuers.set -> void LettuceEncrypt.LettuceEncryptOptions.AllowedChallengeTypes.get -> LettuceEncrypt.Acme.ChallengeType LettuceEncrypt.LettuceEncryptOptions.AllowedChallengeTypes.set -> void -LettuceEncrypt.LettuceEncryptOptions.DomainNames.get -> string![]! +LettuceEncrypt.LettuceEncryptOptions.DomainNames.get -> string![]![]! LettuceEncrypt.LettuceEncryptOptions.DomainNames.set -> void LettuceEncrypt.LettuceEncryptOptions.EabCredentials.get -> LettuceEncrypt.Acme.EabCredentials! LettuceEncrypt.LettuceEncryptOptions.EabCredentials.set -> void diff --git a/test/LettuceEncrypt.Azure.UnitTests/AzureKeyVaultTests.cs b/test/LettuceEncrypt.Azure.UnitTests/AzureKeyVaultTests.cs index 5b3da148..d105d7d8 100644 --- a/test/LettuceEncrypt.Azure.UnitTests/AzureKeyVaultTests.cs +++ b/test/LettuceEncrypt.Azure.UnitTests/AzureKeyVaultTests.cs @@ -71,14 +71,14 @@ public async Task ImportCertificateChecksDuplicate() certClientFactory.Setup(c => c.Create()).Returns(certClient.Object); var options = Options.Create(new LettuceEncryptOptions()); - options.Value.DomainNames = new[] { Domain1, Domain2 }; + options.Value.DomainNames = new[] { new[] { Domain1, Domain2 } }; var repository = new AzureKeyVaultCertificateRepository( certClientFactory.Object, Mock.Of(), options, NullLogger.Instance); - foreach (var domain in options.Value.DomainNames) + foreach (var domain in options.Value.DomainNames.First()) { var certificateToSave = TestUtils.CreateTestCert(domain); await repository.SaveAsync(certificateToSave, CancellationToken.None); @@ -101,7 +101,7 @@ public async Task GetCertificateLooksForDomainsAsync() secretClientFactory.Setup(c => c.Create()).Returns(secretClient.Object); var options = Options.Create(new LettuceEncryptOptions()); - options.Value.DomainNames = new[] { Domain1, Domain2 }; + options.Value.DomainNames = new[] { new[] { Domain1, Domain2 } }; var repository = new AzureKeyVaultCertificateRepository( Mock.Of(), diff --git a/test/LettuceEncrypt.UnitTests/ConfigurationBindingTests.cs b/test/LettuceEncrypt.UnitTests/ConfigurationBindingTests.cs index c310748c..a9fcd441 100644 --- a/test/LettuceEncrypt.UnitTests/ConfigurationBindingTests.cs +++ b/test/LettuceEncrypt.UnitTests/ConfigurationBindingTests.cs @@ -16,15 +16,17 @@ public void ItBindsToConfig() var options = ParseOptions(new() { ["LettuceEncrypt:AcceptTermsOfService"] = "true", - ["LettuceEncrypt:DomainNames:0"] = "one.com", - ["LettuceEncrypt:DomainNames:1"] = "two.com", + ["LettuceEncrypt:DomainNames:0:0"] = "one_one.com", + ["LettuceEncrypt:DomainNames:0:1"] = "one_two.com", + ["LettuceEncrypt:DomainNames:1:0"] = "two_one.com", + ["LettuceEncrypt:DomainNames:1:1"] = "two_two.com", ["LettuceEncrypt:AllowedChallengeTypes"] = "Http01", }); Assert.True(options.AcceptTermsOfService); Assert.Collection(options.DomainNames, - one => Assert.Equal("one.com", one), - two => Assert.Equal("two.com", two)); + one => Assert.Equal(new string[] { "one_one.com", "one_two.com" }, one), + two => Assert.Equal(new string[] { "two_one.com", "two_two.com" }, two)); Assert.Equal(Acme.ChallengeType.Http01, options.AllowedChallengeTypes); } @@ -71,7 +73,7 @@ public void DoesNotSupportWildcardDomains() Assert.Throws(() => ParseOptions(new() { - ["LettuceEncrypt:DomainNames:0"] = "*.natemcmaster.com", + ["LettuceEncrypt:DomainNames:0:0"] = "*.natemcmaster.com", })); } diff --git a/test/LettuceEncrypt.UnitTests/X509CertStoreTests.cs b/test/LettuceEncrypt.UnitTests/X509CertStoreTests.cs index 3e147c5f..60791234 100644 --- a/test/LettuceEncrypt.UnitTests/X509CertStoreTests.cs +++ b/test/LettuceEncrypt.UnitTests/X509CertStoreTests.cs @@ -40,7 +40,7 @@ public void Dispose() public async Task ItFindsCertByCommonNameAsync() { var commonName = "x509store.read.test.natemcmaster.com"; - _options.DomainNames = new[] { commonName }; + _options.DomainNames = new[] { new[] { commonName } }; using var x509Store = new X509Store(StoreName.My, StoreLocation.CurrentUser); x509Store.Open(OpenFlags.ReadWrite); var testCert = CreateTestCert(commonName); @@ -97,7 +97,7 @@ public async Task ItSavesCertificates() public async Task ItReturnsEmptyWhenCantFindCertAsync() { var commonName = "notfound.test.natemcmaster.com"; - _options.DomainNames = new[] { commonName }; + _options.DomainNames = new[] { new[] { commonName } }; var certs = await _certStore.GetCertificatesAsync(default); Assert.Empty(certs); }