diff --git a/docs/gh-build-and-sign.yml b/docs/gh-build-and-sign.yml index b013eeef..ec92e605 100644 --- a/docs/gh-build-and-sign.yml +++ b/docs/gh-build-and-sign.yml @@ -88,6 +88,17 @@ jobs: --azure-credential-type "azure-cli" --azure-key-vault-url "${{ secrets.KEY_VAULT_URL }}" # This does not need to be a secret and is just a placeholder --azure-key-vault-certificate "${{ secrets.KEY_VAULT_CERTIFICATE_ID }}" # This does not need to be a secret and is just a placeholder + --certificate-output "${{ runner.temp }}\signing-certificate.cer" + + # Make the public certificate path available to later steps in this job. + - name: Publish signing certificate path + id: signing-certificate + shell: pwsh + run: '"certificate_file=${{ runner.temp }}\signing-certificate.cer" >> $env:GITHUB_OUTPUT' + + - name: Use signing certificate + shell: pwsh + run: Get-PfxCertificate -FilePath "${{ steps.signing-certificate.outputs.certificate_file }}" # Publish the signed packages - name: Upload build artifacts diff --git a/src/Sign.Cli/ArtifactSigningCommand.cs b/src/Sign.Cli/ArtifactSigningCommand.cs index c0c3f480..188b3567 100644 --- a/src/Sign.Cli/ArtifactSigningCommand.cs +++ b/src/Sign.Cli/ArtifactSigningCommand.cs @@ -103,7 +103,7 @@ internal ArtifactSigningCommand(CodeCommand codeCommand, IServiceProviderFactory ArtifactSigningServiceProvider trustedSigningServiceProvider = new(); - return codeCommand.HandleAsync(parseResult, serviceProviderFactory, trustedSigningServiceProvider, filesArgument); + return codeCommand.HandleAsync(parseResult, serviceProviderFactory, trustedSigningServiceProvider, filesArgument, cancellationToken); }); } } diff --git a/src/Sign.Cli/AzureKeyVaultCommand.cs b/src/Sign.Cli/AzureKeyVaultCommand.cs index 25e9ee82..dd4c0a73 100644 --- a/src/Sign.Cli/AzureKeyVaultCommand.cs +++ b/src/Sign.Cli/AzureKeyVaultCommand.cs @@ -118,7 +118,7 @@ internal AzureKeyVaultCommand(CodeCommand codeCommand, IServiceProviderFactory s KeyVaultServiceProvider keyVaultServiceProvider = new(); - return codeCommand.HandleAsync(parseResult, serviceProviderFactory, keyVaultServiceProvider, filesArgument); + return codeCommand.HandleAsync(parseResult, serviceProviderFactory, keyVaultServiceProvider, filesArgument, cancellationToken); }); } diff --git a/src/Sign.Cli/CertificateStoreCommand.cs b/src/Sign.Cli/CertificateStoreCommand.cs index b6d63dcb..a0604ed9 100644 --- a/src/Sign.Cli/CertificateStoreCommand.cs +++ b/src/Sign.Cli/CertificateStoreCommand.cs @@ -143,7 +143,7 @@ internal CertificateStoreCommand(CodeCommand codeCommand, IServiceProviderFactor useMachineKeyContainer, isInteractive); - return codeCommand.HandleAsync(parseResult, serviceProviderFactory, certificateStoreServiceProvider, filesArgument); + return codeCommand.HandleAsync(parseResult, serviceProviderFactory, certificateStoreServiceProvider, filesArgument, cancellationToken); }); } diff --git a/src/Sign.Cli/CodeCommand.cs b/src/Sign.Cli/CodeCommand.cs index c61367a5..b64851c9 100644 --- a/src/Sign.Cli/CodeCommand.cs +++ b/src/Sign.Cli/CodeCommand.cs @@ -6,6 +6,7 @@ using System.CommandLine.Parsing; using System.Globalization; using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; using System.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileSystemGlobbing; @@ -19,6 +20,7 @@ internal sealed class CodeCommand : Command { internal Option ApplicationNameOption { get; } internal Option BaseDirectoryOption { get; } + internal Option CertificateOutputOption { get; } internal Option DescriptionOption { get; } internal Option DescriptionUrlOption { get; } internal Option FileDigestOption { get; } @@ -46,6 +48,11 @@ internal CodeCommand() Description = Resources.BaseDirectoryOptionDescription, Recursive = true }; + CertificateOutputOption = new Option("--certificate-output", "-co") + { + Description = Resources.CertificateOutputOptionDescription, + Recursive = true + }; DescriptionOption = new Option("--description", "-d") { Description = Resources.DescriptionOptionDescription, @@ -120,6 +127,7 @@ internal CodeCommand() Options.Add(DescriptionUrlOption); Options.Add(BaseDirectoryOption); Options.Add(OutputOption); + Options.Add(CertificateOutputOption); Options.Add(PublisherNameOption); Options.Add(FileListOption); Options.Add(RecurseContainersOption); @@ -130,7 +138,12 @@ internal CodeCommand() Options.Add(VerbosityOption); } - internal async Task HandleAsync(ParseResult parseResult, IServiceProviderFactory serviceProviderFactory, ISignatureProvider signatureProvider, IEnumerable filesArgument) + internal async Task HandleAsync( + ParseResult parseResult, + IServiceProviderFactory serviceProviderFactory, + ISignatureProvider signatureProvider, + IEnumerable filesArgument, + CancellationToken cancellationToken) { // Some of the options have a default value and that is why we can safely use // the null-forgiving operator (!) to simplify the code. @@ -146,6 +159,7 @@ internal async Task HandleAsync(ParseResult parseResult, IServiceProviderFa Uri timestampUrl = parseResult.GetValue(TimestampUrlOption)!; LogLevel verbosity = parseResult.GetValue(VerbosityOption); string? output = parseResult.GetValue(OutputOption); + string? certificateOutput = parseResult.GetValue(CertificateOutputOption); int maxConcurrency = parseResult.GetValue(MaxConcurrencyOption); // Make sure this is rooted @@ -246,6 +260,8 @@ internal async Task HandleAsync(ParseResult parseResult, IServiceProviderFa ISigner signer = serviceProvider.GetRequiredService(); + cancellationToken.ThrowIfCancellationRequested(); + int exitCode = await signer.SignAsync( inputFiles, output, @@ -261,9 +277,33 @@ internal async Task HandleAsync(ParseResult parseResult, IServiceProviderFa fileHashAlgorithmName, timestampHashAlgorithmName); + if (exitCode == ExitCode.Success && !string.IsNullOrEmpty(certificateOutput)) + { + FileInfo certificateOutputFile = new(ExpandFilePath(baseDirectory, certificateOutput)); + ICertificateProvider certificateProvider = serviceProvider.GetRequiredService(); + + await ExportCertificateAsync(certificateProvider, certificateOutputFile, cancellationToken); + } + return exitCode; } + internal static async Task ExportCertificateAsync( + ICertificateProvider certificateProvider, + FileInfo certificateOutputFile, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(certificateProvider, nameof(certificateProvider)); + ArgumentNullException.ThrowIfNull(certificateOutputFile, nameof(certificateOutputFile)); + + certificateOutputFile.Directory!.Create(); + + using X509Certificate2 certificate = await certificateProvider.GetCertificateAsync(cancellationToken); + byte[] certificateBytes = certificate.Export(X509ContentType.Cert); + + await File.WriteAllBytesAsync(certificateOutputFile.FullName, certificateBytes, cancellationToken); + } + private static string ExpandFilePath(DirectoryInfo baseDirectory, string file) { if (Path.IsPathRooted(file)) diff --git a/src/Sign.Cli/Resources.Designer.cs b/src/Sign.Cli/Resources.Designer.cs index 857aed23..c493c2e9 100644 --- a/src/Sign.Cli/Resources.Designer.cs +++ b/src/Sign.Cli/Resources.Designer.cs @@ -77,6 +77,15 @@ internal static string BaseDirectoryOptionDescription { return ResourceManager.GetString("BaseDirectoryOptionDescription", resourceCulture); } } + + /// + /// Looks up a localized string similar to Write the public signing certificate as a DER-encoded CER file.. + /// + internal static string CertificateOutputOptionDescription { + get { + return ResourceManager.GetString("CertificateOutputOptionDescription", resourceCulture); + } + } /// /// Looks up a localized string similar to Use Windows Certificate Store or a local certificate file.. diff --git a/src/Sign.Cli/Resources.resx b/src/Sign.Cli/Resources.resx index 3f730167..681d1bf8 100644 --- a/src/Sign.Cli/Resources.resx +++ b/src/Sign.Cli/Resources.resx @@ -123,6 +123,9 @@ Base directory for files. Overrides the current working directory. + + Write the public signing certificate as a DER-encoded CER file. + Use Windows Certificate Store or a local certificate file. diff --git a/src/Sign.Cli/TrustedSigningCommand.cs b/src/Sign.Cli/TrustedSigningCommand.cs index cc9ca491..ae901d33 100644 --- a/src/Sign.Cli/TrustedSigningCommand.cs +++ b/src/Sign.Cli/TrustedSigningCommand.cs @@ -105,7 +105,7 @@ internal TrustedSigningCommand(CodeCommand codeCommand, IServiceProviderFactory ArtifactSigningServiceProvider trustedSigningServiceProvider = new(); - return codeCommand.HandleAsync(parseResult, serviceProviderFactory, trustedSigningServiceProvider, filesArgument); + return codeCommand.HandleAsync(parseResult, serviceProviderFactory, trustedSigningServiceProvider, filesArgument, cancellationToken); }); } } diff --git a/src/Sign.Cli/xlf/Resources.cs.xlf b/src/Sign.Cli/xlf/Resources.cs.xlf index 6b141693..c23fdadc 100644 --- a/src/Sign.Cli/xlf/Resources.cs.xlf +++ b/src/Sign.Cli/xlf/Resources.cs.xlf @@ -12,6 +12,11 @@ Základní adresář pro soubory Přepíše aktuální pracovní adresář. + + Write the public signing certificate as a DER-encoded CER file. + Write the public signing certificate as a DER-encoded CER file. + + Use Windows Certificate Store or a local certificate file. Použijte úložiště certifikátů systému Windows nebo místní soubor certifikátu. diff --git a/src/Sign.Cli/xlf/Resources.de.xlf b/src/Sign.Cli/xlf/Resources.de.xlf index da4c7469..33f9dde3 100644 --- a/src/Sign.Cli/xlf/Resources.de.xlf +++ b/src/Sign.Cli/xlf/Resources.de.xlf @@ -12,6 +12,11 @@ Basisverzeichnis für Dateien. Überschreibt das aktuelle Arbeitsverzeichnis. + + Write the public signing certificate as a DER-encoded CER file. + Write the public signing certificate as a DER-encoded CER file. + + Use Windows Certificate Store or a local certificate file. Verwenden Sie den Windows-Zertifikatspeicher oder eine lokale Zertifikatdatei. diff --git a/src/Sign.Cli/xlf/Resources.es.xlf b/src/Sign.Cli/xlf/Resources.es.xlf index 16b74a1e..c2b86566 100644 --- a/src/Sign.Cli/xlf/Resources.es.xlf +++ b/src/Sign.Cli/xlf/Resources.es.xlf @@ -12,6 +12,11 @@ Directorio base para los archivos. Invalida el directorio de trabajo actual. + + Write the public signing certificate as a DER-encoded CER file. + Write the public signing certificate as a DER-encoded CER file. + + Use Windows Certificate Store or a local certificate file. Use el Almacén de certificados de Windows o un archivo de certificados local. diff --git a/src/Sign.Cli/xlf/Resources.fr.xlf b/src/Sign.Cli/xlf/Resources.fr.xlf index 5dc6faf2..ef6ecbb6 100644 --- a/src/Sign.Cli/xlf/Resources.fr.xlf +++ b/src/Sign.Cli/xlf/Resources.fr.xlf @@ -12,6 +12,11 @@ Répertoire de base pour les fichiers. Remplace le répertoire de travail actuel. + + Write the public signing certificate as a DER-encoded CER file. + Write the public signing certificate as a DER-encoded CER file. + + Use Windows Certificate Store or a local certificate file. Utilisez le magasin de certificats Windows ou un fichier local du certificat. diff --git a/src/Sign.Cli/xlf/Resources.it.xlf b/src/Sign.Cli/xlf/Resources.it.xlf index 3fe69af9..3e6d184b 100644 --- a/src/Sign.Cli/xlf/Resources.it.xlf +++ b/src/Sign.Cli/xlf/Resources.it.xlf @@ -12,6 +12,11 @@ Directory di base per i file. Esegue l'override della directory di lavoro corrente. + + Write the public signing certificate as a DER-encoded CER file. + Write the public signing certificate as a DER-encoded CER file. + + Use Windows Certificate Store or a local certificate file. Utilizzare l'archivio certificati di Windows o un file di certificato locale. diff --git a/src/Sign.Cli/xlf/Resources.ja.xlf b/src/Sign.Cli/xlf/Resources.ja.xlf index 8a638867..9419f8e4 100644 --- a/src/Sign.Cli/xlf/Resources.ja.xlf +++ b/src/Sign.Cli/xlf/Resources.ja.xlf @@ -12,6 +12,11 @@ ファイルのベース ディレクトリ。 現在の作業ディレクトリをオーバーライドします。 + + Write the public signing certificate as a DER-encoded CER file. + Write the public signing certificate as a DER-encoded CER file. + + Use Windows Certificate Store or a local certificate file. Windows 証明書ストアまたはローカル証明書ファイルを使用します。 diff --git a/src/Sign.Cli/xlf/Resources.ko.xlf b/src/Sign.Cli/xlf/Resources.ko.xlf index 36e0896c..b29ddf64 100644 --- a/src/Sign.Cli/xlf/Resources.ko.xlf +++ b/src/Sign.Cli/xlf/Resources.ko.xlf @@ -12,6 +12,11 @@ 파일의 기본 디렉터리입니다. 현재 작업 디렉터리를 재정의합니다. + + Write the public signing certificate as a DER-encoded CER file. + Write the public signing certificate as a DER-encoded CER file. + + Use Windows Certificate Store or a local certificate file. Windows 인증서 저장소 또는 로컬 인증서 파일을 사용합니다. diff --git a/src/Sign.Cli/xlf/Resources.pl.xlf b/src/Sign.Cli/xlf/Resources.pl.xlf index 3e31c0b9..b5bd1af4 100644 --- a/src/Sign.Cli/xlf/Resources.pl.xlf +++ b/src/Sign.Cli/xlf/Resources.pl.xlf @@ -12,6 +12,11 @@ Katalog podstawowy dla plików. Zastępuje bieżący katalog roboczy. + + Write the public signing certificate as a DER-encoded CER file. + Write the public signing certificate as a DER-encoded CER file. + + Use Windows Certificate Store or a local certificate file. Użyj magazynu certyfikatów systemu Windows lub lokalnego pliku certyfikatów. diff --git a/src/Sign.Cli/xlf/Resources.pt-BR.xlf b/src/Sign.Cli/xlf/Resources.pt-BR.xlf index c9ca5376..cbe09067 100644 --- a/src/Sign.Cli/xlf/Resources.pt-BR.xlf +++ b/src/Sign.Cli/xlf/Resources.pt-BR.xlf @@ -12,6 +12,11 @@ Diretório base para arquivos. Substitui o diretório de trabalho atual. + + Write the public signing certificate as a DER-encoded CER file. + Write the public signing certificate as a DER-encoded CER file. + + Use Windows Certificate Store or a local certificate file. Use o Repositório de Certificados do Windows ou um arquivo de certificado local. diff --git a/src/Sign.Cli/xlf/Resources.ru.xlf b/src/Sign.Cli/xlf/Resources.ru.xlf index 9a24103c..4a45ec61 100644 --- a/src/Sign.Cli/xlf/Resources.ru.xlf +++ b/src/Sign.Cli/xlf/Resources.ru.xlf @@ -12,6 +12,11 @@ Базовый каталог для файлов. Переопределяет текущий рабочий каталог. + + Write the public signing certificate as a DER-encoded CER file. + Write the public signing certificate as a DER-encoded CER file. + + Use Windows Certificate Store or a local certificate file. Используйте хранилище сертификатов Windows или локальный файл сертификата. diff --git a/src/Sign.Cli/xlf/Resources.tr.xlf b/src/Sign.Cli/xlf/Resources.tr.xlf index 5689874b..764881d7 100644 --- a/src/Sign.Cli/xlf/Resources.tr.xlf +++ b/src/Sign.Cli/xlf/Resources.tr.xlf @@ -12,6 +12,11 @@ Dosyalar için temel dizin. Geçerli çalışma dizinini geçersiz kılar. + + Write the public signing certificate as a DER-encoded CER file. + Write the public signing certificate as a DER-encoded CER file. + + Use Windows Certificate Store or a local certificate file. Windows Sertifika Deposu veya yerel bir sertifika dosyası kullanın. diff --git a/src/Sign.Cli/xlf/Resources.zh-Hans.xlf b/src/Sign.Cli/xlf/Resources.zh-Hans.xlf index 47deaec0..48ad2e76 100644 --- a/src/Sign.Cli/xlf/Resources.zh-Hans.xlf +++ b/src/Sign.Cli/xlf/Resources.zh-Hans.xlf @@ -12,6 +12,11 @@ 文件的基目录。替代当前工作目录。 + + Write the public signing certificate as a DER-encoded CER file. + Write the public signing certificate as a DER-encoded CER file. + + Use Windows Certificate Store or a local certificate file. 使用 Windows 证书存储或本地证书文件。 diff --git a/src/Sign.Cli/xlf/Resources.zh-Hant.xlf b/src/Sign.Cli/xlf/Resources.zh-Hant.xlf index 0427c8c6..3394389e 100644 --- a/src/Sign.Cli/xlf/Resources.zh-Hant.xlf +++ b/src/Sign.Cli/xlf/Resources.zh-Hant.xlf @@ -12,6 +12,11 @@ 檔案的基底目錄。覆寫目前的工作目錄。 + + Write the public signing certificate as a DER-encoded CER file. + Write the public signing certificate as a DER-encoded CER file. + + Use Windows Certificate Store or a local certificate file. 使用 Windows 憑證存放區或本機憑證檔案。 diff --git a/test/Sign.Cli.Test/CodeCommandTests.cs b/test/Sign.Cli.Test/CodeCommandTests.cs index e7bf8d2b..c966ad6c 100644 --- a/test/Sign.Cli.Test/CodeCommandTests.cs +++ b/test/Sign.Cli.Test/CodeCommandTests.cs @@ -3,6 +3,11 @@ // See the LICENSE.txt file in the project root for more information. using System.CommandLine; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Sign.Core; namespace Sign.Cli.Test { @@ -22,6 +27,126 @@ public void BaseDirectoryOption_Always_IsNotRequired() Assert.False(_command.BaseDirectoryOption.Required); } + [Fact] + public void CertificateOutputOption_Always_HasArityOfExactlyOne() + { + Assert.Equal(ArgumentArity.ExactlyOne, _command.CertificateOutputOption.Arity); + } + + [Fact] + public void CertificateOutputOption_Always_IsNotRequired() + { + Assert.False(_command.CertificateOutputOption.Required); + } + + [Fact] + public async Task ExportCertificateAsync_WritesThePublicCertificateAsCer() + { + using RSA key = RSA.Create(2048); + CertificateRequest request = new("CN=test", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + using X509Certificate2 expectedCertificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-1), DateTimeOffset.UtcNow.AddMinutes(1)); + Mock certificateProvider = new(); + certificateProvider.Setup(provider => provider.GetCertificateAsync(It.IsAny())) + .ReturnsAsync(new X509Certificate2(expectedCertificate.Export(X509ContentType.Pfx))); + + string outputDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + FileInfo outputFile = new(Path.Combine(outputDirectory, "certificate.cer")); + + try + { + await CodeCommand.ExportCertificateAsync(certificateProvider.Object, outputFile, CancellationToken.None); + + using X509Certificate2 actualCertificate = new(outputFile.FullName); + Assert.Equal(expectedCertificate.Thumbprint, actualCertificate.Thumbprint); + Assert.False(actualCertificate.HasPrivateKey); + } + finally + { + if (Directory.Exists(outputDirectory)) + { + Directory.Delete(outputDirectory, recursive: true); + } + } + } + + [Fact] + public async Task HandleAsync_WhenSigningSucceeds_WritesCertificateToRelativeOutputPath() + { + using RSA key = RSA.Create(2048); + CertificateRequest request = new("CN=test", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + using X509Certificate2 expectedCertificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-1), DateTimeOffset.UtcNow.AddMinutes(1)); + Mock certificateProvider = new(); + certificateProvider.Setup(provider => provider.GetCertificateAsync(It.IsAny())) + .ReturnsAsync(new X509Certificate2(expectedCertificate.Export(X509ContentType.Pfx))); + SignerSpy signer = new(); + string outputRelativePath = Path.Combine("certificates", "signing.cer"); + + await WithTemporaryInputAsync(async (baseDirectory, inputFile) => + { + int exitCode = await HandleAsync(baseDirectory, inputFile, outputRelativePath, signer, certificateProvider.Object); + FileInfo outputFile = new(Path.Combine(baseDirectory.FullName, outputRelativePath)); + + Assert.Equal(ExitCode.Success, exitCode); + Assert.NotNull(signer.InputFiles); + Assert.True(outputFile.Exists); + + using X509Certificate2 actualCertificate = new(outputFile.FullName); + Assert.Equal(expectedCertificate.Thumbprint, actualCertificate.Thumbprint); + Assert.False(actualCertificate.HasPrivateKey); + }); + } + + [Fact] + public async Task HandleAsync_WhenSigningFails_DoesNotWriteCertificate() + { + Mock certificateProvider = new(); + SignerSpy signer = new(ExitCode.Failed); + string outputRelativePath = Path.Combine("certificates", "signing.cer"); + + await WithTemporaryInputAsync(async (baseDirectory, inputFile) => + { + int exitCode = await HandleAsync(baseDirectory, inputFile, outputRelativePath, signer, certificateProvider.Object); + FileInfo outputFile = new(Path.Combine(baseDirectory.FullName, outputRelativePath)); + + Assert.Equal(ExitCode.Failed, exitCode); + Assert.NotNull(signer.InputFiles); + Assert.False(outputFile.Exists); + certificateProvider.Verify( + provider => provider.GetCertificateAsync(It.IsAny()), + Times.Never); + }); + } + + [Fact] + public async Task HandleAsync_WhenCancelled_DoesNotSignOrWriteCertificate() + { + Mock certificateProvider = new(); + SignerSpy signer = new(); + string outputRelativePath = Path.Combine("certificates", "signing.cer"); + using CancellationTokenSource cancellationTokenSource = new(); + cancellationTokenSource.Cancel(); + + await WithTemporaryInputAsync(async (baseDirectory, inputFile) => + { + await Assert.ThrowsAnyAsync( + () => HandleAsync( + baseDirectory, + inputFile, + outputRelativePath, + signer, + certificateProvider.Object, + cancellationTokenSource.Token)); + + FileInfo outputFile = new(Path.Combine(baseDirectory.FullName, outputRelativePath)); + + Assert.Null(signer.InputFiles); + Assert.False(outputFile.Exists); + certificateProvider.Verify( + provider => provider.GetCertificateAsync(It.IsAny()), + Times.Never); + }); + } + [Fact] public void DescriptionOption_Always_HasArityOfExactlyOne() { @@ -141,5 +266,51 @@ public void VerbosityOption_Always_IsNotRequired() { Assert.False(_command.VerbosityOption.Required); } + + private async Task HandleAsync( + DirectoryInfo baseDirectory, + FileInfo inputFile, + string certificateOutput, + SignerSpy signer, + ICertificateProvider certificateProvider, + CancellationToken cancellationToken = default) + { + ServiceCollection services = new(); + services.AddSingleton(signer); + services.AddSingleton(certificateProvider); + + using var serviceProvider = services.BuildServiceProvider(); + TestServiceProviderFactory serviceProviderFactory = new(serviceProvider); + RootCommand rootCommand = new(); + rootCommand.Subcommands.Add(_command); + var parseResult = rootCommand.Parse( + $"code --base-directory \"{baseDirectory.FullName}\" --certificate-output \"{certificateOutput}\""); + + Assert.Empty(parseResult.Errors); + + return await _command.HandleAsync( + parseResult, + serviceProviderFactory, + Mock.Of(), + [inputFile.FullName], + cancellationToken); + } + + private static async Task WithTemporaryInputAsync(Func test) + { + DirectoryInfo baseDirectory = Directory.CreateDirectory( + Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))); + FileInfo inputFile = new(Path.Combine(baseDirectory.FullName, "input.bin")); + + try + { + await File.WriteAllTextAsync(inputFile.FullName, "content"); + await test(baseDirectory, inputFile); + } + finally + { + baseDirectory.Delete(recursive: true); + } + } } } diff --git a/test/Sign.Cli.Test/Options/CertificateOutputOptionTests.cs b/test/Sign.Cli.Test/Options/CertificateOutputOptionTests.cs new file mode 100644 index 00000000..60035e51 --- /dev/null +++ b/test/Sign.Cli.Test/Options/CertificateOutputOptionTests.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE.txt file in the project root for more information. + +namespace Sign.Cli.Test +{ + public class CertificateOutputOptionTests : OptionTests + { + private const string ExpectedValue = "certificate.cer"; + + public CertificateOutputOptionTests() + : base(new CodeCommand().CertificateOutputOption, "-co", "--certificate-output", ExpectedValue) + { + } + } +} diff --git a/test/Sign.Cli.Test/SignCommandTests.cs b/test/Sign.Cli.Test/SignCommandTests.cs index 502c2622..3b56cbb9 100644 --- a/test/Sign.Cli.Test/SignCommandTests.cs +++ b/test/Sign.Cli.Test/SignCommandTests.cs @@ -90,6 +90,7 @@ public void Command_WhenAllOptionsAndArgumentAreValid_HasNoError() Assert.Equal(DescriptionUrl, result.GetValue(_codeCommand.DescriptionUrlOption)!.OriginalString); Assert.Equal(KeyVaultUrl, result.GetValue(_azureKeyVaultCommand.UrlOption)!.OriginalString); Assert.Equal(CertificateName, result.GetValue(_azureKeyVaultCommand.CertificateOption)); + Assert.Null(result.GetValue(_codeCommand.CertificateOutputOption)); Assert.Equal(TimestampUrl, result.GetValue(_codeCommand.TimestampUrlOption)!.OriginalString); Assert.Equal([File], result.GetValue(_azureKeyVaultCommand.FilesArgument)); } diff --git a/test/Sign.Cli.Test/TestInfrastructure/SignerSpy.cs b/test/Sign.Cli.Test/TestInfrastructure/SignerSpy.cs index ec234bf5..f000cd61 100644 --- a/test/Sign.Cli.Test/TestInfrastructure/SignerSpy.cs +++ b/test/Sign.Cli.Test/TestInfrastructure/SignerSpy.cs @@ -24,9 +24,9 @@ internal sealed class SignerSpy : ISigner internal HashAlgorithmName? TimestampHashAlgorithm { get; private set; } internal int ExitCode { get; } - internal SignerSpy() + internal SignerSpy(int exitCode = Core.ExitCode.Success) { - ExitCode = Core.ExitCode.Success; + ExitCode = exitCode; } public Task SignAsync(