@@ -655,6 +655,126 @@ public async Task Restore_bicepparam_should_fail_with_error_diagnostics_for_regi
655655 result . Stderr . Should ( ) . Contain ( "main.bicepparam(1,7) : Error BCP192: Unable to restore the artifact with reference \" br:mockregistry.io/parameters/basic:v1\" : Mock registry request failure." ) ;
656656 }
657657
658+ // ── Trusted Registry (BCP446 / BCP447) tests ─────────────────────────
659+
660+ [ TestMethod ]
661+ public async Task Restore_UntrustedRegistry_EmitsBcp446_ExitCodeOne ( )
662+ {
663+ // Use an arbitrary hostname that is NOT in the built-in trusted list and NOT in the module's bicepconfig.json
664+ var registry = "untrusted.example.com" ;
665+ var repository = "mymodule" ;
666+
667+ // The clientFactory is not expected to be called because enforcement blocks before network I/O.
668+ var clientFactory = StrictMock . Of < IContainerRegistryClientFactory > ( ) ;
669+ var templateSpecRepositoryFactory = StrictMock . Of < ITemplateSpecRepositoryFactory > ( ) ;
670+
671+ var tempDirectory = FileHelper . GetUniqueTestOutputPath ( TestContext ) ;
672+ Directory . CreateDirectory ( tempDirectory ) ;
673+
674+ var bicepFilePath = Path . Combine ( tempDirectory , "main.bicep" ) ;
675+ File . WriteAllText ( bicepFilePath , $ "module mod 'br:{ registry } /{ repository } :v1' = {{ name: 'mod' }}") ;
676+
677+ var settings = new InvocationSettings ( new ( TestContext , RegistryEnabled : true ) , clientFactory . Object , templateSpecRepositoryFactory . Object ) ;
678+ var ( output , error , result ) = await Bicep ( settings , "restore" , bicepFilePath ) ;
679+
680+ using ( new AssertionScope ( ) )
681+ {
682+ result . Should ( ) . Be ( 1 ) ;
683+ output . Should ( ) . BeEmpty ( ) ;
684+ error . Should ( ) . Contain ( "BCP446" ) ;
685+ error . Should ( ) . Contain ( registry ) ;
686+ }
687+ }
688+
689+ [ TestMethod ]
690+ public async Task Restore_UntrustedRegistry_AddedToTrustedRegistries_Succeeds ( )
691+ {
692+ var registry = "mycompany.example.com" ;
693+ var repository = "mymodule" ;
694+ var registryUri = new Uri ( $ "https://{ registry } ") ;
695+
696+ // Publish a real (mock) module so restore can succeed
697+ var client = new FakeRegistryBlobClient ( ) ;
698+ var clientFactory = StrictMock . Of < IContainerRegistryClientFactory > ( ) ;
699+ clientFactory
700+ . Setup ( m => m . CreateAuthenticatedBlobClient ( It . IsAny < CloudConfiguration > ( ) , registryUri , repository ) )
701+ . Returns ( client ) ;
702+
703+ var tempDirectory = FileHelper . GetUniqueTestOutputPath ( TestContext ) ;
704+ Directory . CreateDirectory ( tempDirectory ) ;
705+
706+ var publishedBicepFilePath = Path . Combine ( tempDirectory , "module.bicep" ) ;
707+ File . WriteAllText ( publishedBicepFilePath , "output hello string = 'world'" ) ;
708+
709+ var templateSpecRepositoryFactory = BicepTestConstants . TemplateSpecRepositoryFactory ;
710+ var publishSettings = new InvocationSettings ( new ( TestContext , RegistryEnabled : true ) , clientFactory . Object , templateSpecRepositoryFactory ) ;
711+
712+ // Publish the module
713+ var ( _, _, publishResult ) = await Bicep ( publishSettings , "publish" , publishedBicepFilePath , "--target" , $ "br:{ registry } /{ repository } :v1") ;
714+ publishResult . Should ( ) . Be ( 0 ) ;
715+
716+ // Write a bicepconfig.json that adds the registry to trustedRegistries
717+ var bicepConfigPath = Path . Combine ( tempDirectory , "bicepconfig.json" ) ;
718+ File . WriteAllText ( bicepConfigPath , $$ """
719+ {
720+ "security": {
721+ "trustedRegistries": ["{{ registry }} "]
722+ }
723+ }
724+ """ ) ;
725+
726+ var bicepFilePath = Path . Combine ( tempDirectory , "main.bicep" ) ;
727+ File . WriteAllText ( bicepFilePath , $ "module mod 'br:{ registry } /{ repository } :v1' = {{ name: 'mod' }}") ;
728+
729+ var restoreSettings = new InvocationSettings ( new ( TestContext , RegistryEnabled : true ) , clientFactory . Object , templateSpecRepositoryFactory ) ;
730+ var ( output , error , result ) = await Bicep ( restoreSettings , "restore" , bicepFilePath ) ;
731+
732+ using ( new AssertionScope ( ) )
733+ {
734+ result . Should ( ) . Be ( 0 , $ "restore should succeed when registry is in trustedRegistries; stderr was: { error } ") ;
735+ output . Should ( ) . BeEmpty ( ) ;
736+ error . Should ( ) . BeEmpty ( ) ;
737+ }
738+ }
739+
740+ [ TestMethod ]
741+ public async Task Restore_InvalidTrustedRegistriesPattern_EmitsBcp447_ExitCodeOne ( )
742+ {
743+ // Use a registry hostname that IS trusted by default but the bicepconfig has an invalid pattern
744+ var registry = "contoso.azurecr.io" ;
745+ var repository = "mymodule" ;
746+
747+ var clientFactory = StrictMock . Of < IContainerRegistryClientFactory > ( ) ;
748+ var templateSpecRepositoryFactory = StrictMock . Of < ITemplateSpecRepositoryFactory > ( ) ;
749+
750+ var tempDirectory = FileHelper . GetUniqueTestOutputPath ( TestContext ) ;
751+ Directory . CreateDirectory ( tempDirectory ) ;
752+
753+ // Write a bicepconfig.json with an invalid pattern (single-label wildcard *.io is rejected)
754+ var bicepConfigPath = Path . Combine ( tempDirectory , "bicepconfig.json" ) ;
755+ File . WriteAllText ( bicepConfigPath , """
756+ {
757+ "security": {
758+ "trustedRegistries": ["*.io"]
759+ }
760+ }
761+ """ ) ;
762+
763+ var bicepFilePath = Path . Combine ( tempDirectory , "main.bicep" ) ;
764+ File . WriteAllText ( bicepFilePath , $ "module mod 'br:{ registry } /{ repository } :v1' = {{ name: 'mod' }}") ;
765+
766+ var settings = new InvocationSettings ( new ( TestContext , RegistryEnabled : true ) , clientFactory . Object , templateSpecRepositoryFactory . Object ) ;
767+ var ( output , error , result ) = await Bicep ( settings , "restore" , bicepFilePath ) ;
768+
769+ using ( new AssertionScope ( ) )
770+ {
771+ result . Should ( ) . Be ( 1 ) ;
772+ output . Should ( ) . BeEmpty ( ) ;
773+ error . Should ( ) . Contain ( "BCP447" ) ;
774+ error . Should ( ) . Contain ( "*.io" ) ;
775+ }
776+ }
777+
658778 private static IEnumerable < object [ ] > GetAllDataSetsWithPublishSource ( )
659779 {
660780 foreach ( DataSet ds in DataSets . AllDataSets )
0 commit comments