@@ -518,6 +518,271 @@ public void GetKeyVaultManager_ThrowsWhenNoKeyVaultManagerOrAccessTokenProvided(
518518 }
519519 }
520520
521+ [ Test ]
522+ [ TestCase ( true , "testCert.pfx" ) ]
523+ [ TestCase ( false , "testCert.cer" ) ]
524+ public async Task ExecuteAsync_SavesCertificateToDownloadDirectory ( bool withPrivateKey , string expectedFileName )
525+ {
526+ this . mockFixture . Setup ( PlatformID . Win32NT ) ;
527+ string downloadDir = "/tmp/certificates" ;
528+ string expectedPath = this . mockFixture . Combine ( downloadDir , expectedFileName ) ;
529+
530+ this . mockFixture . Parameters = new Dictionary < string , IConvertible > ( )
531+ {
532+ { nameof ( CertificateInstallation . CertificateName ) , "testCert" } ,
533+ { nameof ( CertificateInstallation . KeyVaultUri ) , "https://testvault.vault.azure.net/" } ,
534+ { nameof ( CertificateInstallation . CertificateDownloadDir ) , downloadDir } ,
535+ { nameof ( CertificateInstallation . WithPrivateKey ) , withPrivateKey }
536+ } ;
537+
538+ this . mockFixture . File . Setup ( f => f . Exists ( expectedPath ) ) . Returns ( false ) ;
539+ this . mockFixture . File . Setup ( f => f . WriteAllBytesAsync ( expectedPath , It . IsAny < byte [ ] > ( ) , It . IsAny < CancellationToken > ( ) ) )
540+ . Returns ( Task . CompletedTask ) ;
541+
542+ using ( TestCertificateInstallation component = new TestCertificateInstallation ( this . mockFixture . Dependencies , this . mockFixture . Parameters ) )
543+ {
544+ this . mockFixture . KeyVaultManager
545+ . Setup ( m => m . GetCertificateAsync (
546+ It . IsAny < string > ( ) ,
547+ It . IsAny < CancellationToken > ( ) ,
548+ It . IsAny < string > ( ) ,
549+ It . IsAny < bool > ( ) ,
550+ It . IsAny < IAsyncPolicy > ( ) ) )
551+ . ReturnsAsync ( this . testCertificate ) ;
552+
553+ component . OnInstallCertificateOnWindows = ( cert , token ) => Task . CompletedTask ;
554+
555+ await component . ExecuteAsync ( EventContext . None , CancellationToken . None ) ;
556+ }
557+
558+ // Verify the file was written with the correct path
559+ this . mockFixture . File . Verify (
560+ f => f . WriteAllBytesAsync ( expectedPath , It . IsAny < byte [ ] > ( ) , It . IsAny < CancellationToken > ( ) ) ,
561+ Times . Once ) ;
562+ }
563+
564+ [ Test ]
565+ public async Task ExecuteAsync_SavesPrivateCertificateAsPfxFormat ( )
566+ {
567+ this . mockFixture . Setup ( PlatformID . Win32NT ) ;
568+ this . SetupPrivateCertificate ( ) ;
569+
570+ string downloadDir = "/tmp/certificates" ;
571+ string expectedPath = this . mockFixture . Combine ( downloadDir , "testCert.pfx" ) ;
572+ byte [ ] capturedBytes = null ;
573+
574+ this . mockFixture . Parameters = new Dictionary < string , IConvertible > ( )
575+ {
576+ { nameof ( CertificateInstallation . CertificateName ) , "testCert" } ,
577+ { nameof ( CertificateInstallation . KeyVaultUri ) , "https://testvault.vault.azure.net/" } ,
578+ { nameof ( CertificateInstallation . CertificateDownloadDir ) , downloadDir } ,
579+ { nameof ( CertificateInstallation . WithPrivateKey ) , true }
580+ } ;
581+
582+ this . mockFixture . File . Setup ( f => f . Exists ( expectedPath ) ) . Returns ( false ) ;
583+ this . mockFixture . File . Setup ( f => f . WriteAllBytesAsync ( expectedPath , It . IsAny < byte [ ] > ( ) , It . IsAny < CancellationToken > ( ) ) )
584+ . Callback < string , byte [ ] , CancellationToken > ( ( path , bytes , token ) => capturedBytes = bytes )
585+ . Returns ( Task . CompletedTask ) ;
586+
587+ using ( TestCertificateInstallation component = new TestCertificateInstallation ( this . mockFixture . Dependencies , this . mockFixture . Parameters ) )
588+ {
589+ this . mockFixture . KeyVaultManager
590+ . Setup ( m => m . GetCertificateAsync (
591+ It . IsAny < string > ( ) ,
592+ It . IsAny < CancellationToken > ( ) ,
593+ It . IsAny < string > ( ) ,
594+ It . IsAny < bool > ( ) ,
595+ It . IsAny < IAsyncPolicy > ( ) ) )
596+ . ReturnsAsync ( this . testCertificate ) ;
597+
598+ component . OnInstallCertificateOnWindows = ( cert , token ) => Task . CompletedTask ;
599+
600+ await component . ExecuteAsync ( EventContext . None , CancellationToken . None ) ;
601+ }
602+
603+ // Verify the bytes captured are in PFX format
604+ Assert . IsNotNull ( capturedBytes ) ;
605+ byte [ ] expectedBytes = this . testCertificate . Export ( X509ContentType . Pfx , string . Empty ) ;
606+ Assert . AreEqual ( expectedBytes . Length , capturedBytes . Length ) ;
607+ }
608+
609+ [ Test ]
610+ public async Task ExecuteAsync_SavesPublicCertificateAsCerFormat ( )
611+ {
612+ this . mockFixture . Setup ( PlatformID . Win32NT ) ;
613+ string downloadDir = "/tmp/certificates" ;
614+ string expectedPath = this . mockFixture . Combine ( downloadDir , "testCert.cer" ) ;
615+ byte [ ] capturedBytes = null ;
616+
617+ this . mockFixture . Parameters = new Dictionary < string , IConvertible > ( )
618+ {
619+ { nameof ( CertificateInstallation . CertificateName ) , "testCert" } ,
620+ { nameof ( CertificateInstallation . KeyVaultUri ) , "https://testvault.vault.azure.net/" } ,
621+ { nameof ( CertificateInstallation . CertificateDownloadDir ) , downloadDir } ,
622+ { nameof ( CertificateInstallation . WithPrivateKey ) , false }
623+ } ;
624+
625+ this . mockFixture . File . Setup ( f => f . Exists ( expectedPath ) ) . Returns ( false ) ;
626+ this . mockFixture . File . Setup ( f => f . WriteAllBytesAsync ( expectedPath , It . IsAny < byte [ ] > ( ) , It . IsAny < CancellationToken > ( ) ) )
627+ . Callback < string , byte [ ] , CancellationToken > ( ( path , bytes , token ) => capturedBytes = bytes )
628+ . Returns ( Task . CompletedTask ) ;
629+
630+ using ( TestCertificateInstallation component = new TestCertificateInstallation ( this . mockFixture . Dependencies , this . mockFixture . Parameters ) )
631+ {
632+ this . mockFixture . KeyVaultManager
633+ . Setup ( m => m . GetCertificateAsync (
634+ It . IsAny < string > ( ) ,
635+ It . IsAny < CancellationToken > ( ) ,
636+ It . IsAny < string > ( ) ,
637+ It . IsAny < bool > ( ) ,
638+ It . IsAny < IAsyncPolicy > ( ) ) )
639+ . ReturnsAsync ( this . testCertificate ) ;
640+
641+ component . OnInstallCertificateOnWindows = ( cert , token ) => Task . CompletedTask ;
642+
643+ await component . ExecuteAsync ( EventContext . None , CancellationToken . None ) ;
644+ }
645+
646+ // Verify the bytes captured are in Cert format
647+ Assert . IsNotNull ( capturedBytes ) ;
648+ byte [ ] expectedBytes = this . testCertificate . Export ( X509ContentType . Cert , string . Empty ) ;
649+ CollectionAssert . AreEqual ( expectedBytes , capturedBytes ) ;
650+ }
651+
652+ [ Test ]
653+ public async Task ExecuteAsync_DeletesExistingCertificateFileBeforeWriting ( )
654+ {
655+ this . mockFixture . Setup ( PlatformID . Win32NT ) ;
656+ string downloadDir = "/tmp/certificates" ;
657+ string expectedPath = this . mockFixture . Combine ( downloadDir , "testCert.pfx" ) ;
658+
659+ this . mockFixture . Parameters = new Dictionary < string , IConvertible > ( )
660+ {
661+ { nameof ( CertificateInstallation . CertificateName ) , "testCert" } ,
662+ { nameof ( CertificateInstallation . KeyVaultUri ) , "https://testvault.vault.azure.net/" } ,
663+ { nameof ( CertificateInstallation . CertificateDownloadDir ) , downloadDir } ,
664+ { nameof ( CertificateInstallation . WithPrivateKey ) , true }
665+ } ;
666+
667+ // Setup file exists to return true
668+ this . mockFixture . File . Setup ( f => f . Exists ( expectedPath ) ) . Returns ( true ) ;
669+ this . mockFixture . File . Setup ( f => f . Delete ( expectedPath ) ) ;
670+ this . mockFixture . File . Setup ( f => f . WriteAllBytesAsync ( expectedPath , It . IsAny < byte [ ] > ( ) , It . IsAny < CancellationToken > ( ) ) )
671+ . Returns ( Task . CompletedTask ) ;
672+
673+ using ( TestCertificateInstallation component = new TestCertificateInstallation ( this . mockFixture . Dependencies , this . mockFixture . Parameters ) )
674+ {
675+ this . mockFixture . KeyVaultManager
676+ . Setup ( m => m . GetCertificateAsync (
677+ It . IsAny < string > ( ) ,
678+ It . IsAny < CancellationToken > ( ) ,
679+ It . IsAny < string > ( ) ,
680+ It . IsAny < bool > ( ) ,
681+ It . IsAny < IAsyncPolicy > ( ) ) )
682+ . ReturnsAsync ( this . testCertificate ) ;
683+
684+ component . OnInstallCertificateOnWindows = ( cert , token ) => Task . CompletedTask ;
685+
686+ await component . ExecuteAsync ( EventContext . None , CancellationToken . None ) ;
687+ }
688+
689+ // Verify the existing file was deleted before writing
690+ this . mockFixture . File . Verify ( f => f . Delete ( expectedPath ) , Times . Once ) ;
691+ this . mockFixture . File . Verify (
692+ f => f . WriteAllBytesAsync ( expectedPath , It . IsAny < byte [ ] > ( ) , It . IsAny < CancellationToken > ( ) ) ,
693+ Times . Once ) ;
694+ }
695+
696+ [ Test ]
697+ public async Task ExecuteAsync_DoesNotSaveCertificateWhenDownloadDirNotProvided ( )
698+ {
699+ this . mockFixture . Setup ( PlatformID . Win32NT ) ;
700+
701+ this . mockFixture . Parameters = new Dictionary < string , IConvertible > ( )
702+ {
703+ { nameof ( CertificateInstallation . CertificateName ) , "testCert" } ,
704+ { nameof ( CertificateInstallation . KeyVaultUri ) , "https://testvault.vault.azure.net/" } ,
705+ { nameof ( CertificateInstallation . WithPrivateKey ) , true }
706+ // CertificateDownloadDir is not provided
707+ } ;
708+
709+ using ( TestCertificateInstallation component = new TestCertificateInstallation ( this . mockFixture . Dependencies , this . mockFixture . Parameters ) )
710+ {
711+ this . mockFixture . KeyVaultManager
712+ . Setup ( m => m . GetCertificateAsync (
713+ It . IsAny < string > ( ) ,
714+ It . IsAny < CancellationToken > ( ) ,
715+ It . IsAny < string > ( ) ,
716+ It . IsAny < bool > ( ) ,
717+ It . IsAny < IAsyncPolicy > ( ) ) )
718+ . ReturnsAsync ( this . testCertificate ) ;
719+
720+ component . OnInstallCertificateOnWindows = ( cert , token ) => Task . CompletedTask ;
721+
722+ await component . ExecuteAsync ( EventContext . None , CancellationToken . None ) ;
723+ }
724+
725+ // Verify no file operations were performed
726+ this . mockFixture . File . Verify ( f => f . Exists ( It . IsAny < string > ( ) ) , Times . Never ) ;
727+ this . mockFixture . File . Verify ( f => f . Delete ( It . IsAny < string > ( ) ) , Times . Never ) ;
728+ this . mockFixture . File . Verify (
729+ f => f . WriteAllBytesAsync ( It . IsAny < string > ( ) , It . IsAny < byte [ ] > ( ) , It . IsAny < CancellationToken > ( ) ) ,
730+ Times . Never ) ;
731+ }
732+
733+ [ Test ]
734+ public async Task ExecuteAsync_SavesCertificateOnUnixPlatform ( )
735+ {
736+ this . mockFixture . Setup ( PlatformID . Unix ) ;
737+ string downloadDir = "/tmp/certificates" ;
738+ string expectedPath = this . mockFixture . Combine ( downloadDir , "testCert.pfx" ) ;
739+
740+ this . mockFixture . Parameters = new Dictionary < string , IConvertible > ( )
741+ {
742+ { nameof ( CertificateInstallation . CertificateName ) , "testCert" } ,
743+ { nameof ( CertificateInstallation . KeyVaultUri ) , "https://testvault.vault.azure.net/" } ,
744+ { nameof ( CertificateInstallation . CertificateDownloadDir ) , downloadDir } ,
745+ { nameof ( CertificateInstallation . WithPrivateKey ) , true }
746+ } ;
747+
748+ this . mockFixture . File . Setup ( f => f . Exists ( expectedPath ) ) . Returns ( false ) ;
749+ this . mockFixture . File . Setup ( f => f . WriteAllBytesAsync ( expectedPath , It . IsAny < byte [ ] > ( ) , It . IsAny < CancellationToken > ( ) ) )
750+ . Returns ( Task . CompletedTask ) ;
751+
752+ using ( TestCertificateInstallation component = new TestCertificateInstallation ( this . mockFixture . Dependencies , this . mockFixture . Parameters ) )
753+ {
754+ this . mockFixture . KeyVaultManager
755+ . Setup ( m => m . GetCertificateAsync (
756+ It . IsAny < string > ( ) ,
757+ It . IsAny < CancellationToken > ( ) ,
758+ It . IsAny < string > ( ) ,
759+ It . IsAny < bool > ( ) ,
760+ It . IsAny < IAsyncPolicy > ( ) ) )
761+ . ReturnsAsync ( this . testCertificate ) ;
762+
763+ component . OnInstallCertificateOnUnix = ( cert , token ) => Task . CompletedTask ;
764+
765+ await component . ExecuteAsync ( EventContext . None , CancellationToken . None ) ;
766+ }
767+
768+ // Verify certificate was saved to download directory even on Unix
769+ this . mockFixture . File . Verify (
770+ f => f . WriteAllBytesAsync ( expectedPath , It . IsAny < byte [ ] > ( ) , It . IsAny < CancellationToken > ( ) ) ,
771+ Times . Once ) ;
772+ }
773+
774+ private void SetupPrivateCertificate ( )
775+ {
776+ var distinguishedName = new X500DistinguishedName ( "CN=TestCert" ) ;
777+
778+ using var rsa = RSA . Create ( 2048 ) ;
779+ var request = new CertificateRequest ( distinguishedName , rsa , HashAlgorithmName . SHA256 , RSASignaturePadding . Pkcs1 ) ;
780+
781+ this . testCertificate = request . CreateSelfSigned (
782+ DateTimeOffset . UtcNow . AddDays ( - 1 ) ,
783+ DateTimeOffset . UtcNow . AddYears ( 1 ) ) ;
784+ }
785+
521786 private class TestCertificateInstallation : CertificateInstallation
522787 {
523788 public TestCertificateInstallation ( IServiceCollection dependencies , IDictionary < string , IConvertible > parameters )
0 commit comments