22// The .NET Foundation licenses this file to you under the MIT license.
33
44using System ;
5+ using System . Collections . Generic ;
6+ using System . Threading ;
57using System . Threading . Tasks ;
68using Azure . Core ;
79using Azure . ResourceManager ;
810using Azure . ResourceManager . ContainerRegistry . Models ;
911using Microsoft . DotNet . ImageBuilder . Configuration ;
12+ using Microsoft . DotNet . ImageBuilder . Oras ;
1013using Microsoft . DotNet . ImageBuilder . Tests . Helpers ;
1114using Microsoft . Extensions . Logging ;
1215using Moq ;
@@ -29,6 +32,7 @@ public async Task ImportImageAsync_DryRun_DoesNotRequirePublishConfiguration()
2932 var service = new CopyImageService (
3033 Mock . Of < ILogger < CopyImageService > > ( ) ,
3134 Mock . Of < IAcrImageImporter > ( ) ,
35+ Mock . Of < IOrasService > ( ) ,
3236 ConfigurationHelper . CreateOptionsMock ( emptyConfig ) ) ;
3337
3438 await Should . NotThrowAsync ( ( ) =>
@@ -69,10 +73,15 @@ public async Task ImportImageAsync_ExternalSourceRegistry_DoesNotRequireSourceRe
6973 } ;
7074
7175 var mockImporter = new Mock < IAcrImageImporter > ( ) ;
76+ var mockOras = new Mock < IOrasService > ( ) ;
77+ mockOras
78+ . Setup ( o => o . GetReferrersAsync ( It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) )
79+ . ReturnsAsync ( Array . Empty < string > ( ) ) ;
7280
7381 var service = new CopyImageService (
7482 Mock . Of < ILogger < CopyImageService > > ( ) ,
7583 mockImporter . Object ,
84+ mockOras . Object ,
7685 ConfigurationHelper . CreateOptionsMock ( publishConfig ) ) ;
7786
7887 await service . ImportImageAsync (
@@ -92,4 +101,156 @@ await service.ImportImageAsync(
92101 c . Source . RegistryAddress == "docker.io" && c . Source . ResourceId == null ! ) ) ,
93102 Times . Once ) ;
94103 }
104+
105+ /// <summary>
106+ /// When referrers exist for the source image, ImportImageAsync should import each referrer
107+ /// as an untagged artifact in addition to the main image.
108+ /// </summary>
109+ [ Fact ]
110+ public async Task ImportImageAsync_CopiesReferrersAlongWithSourceImage ( )
111+ {
112+ PublishConfiguration publishConfig = CreateAcrPublishConfig ( "myacr.azurecr.io" ) ;
113+
114+ var mockImporter = new Mock < IAcrImageImporter > ( ) ;
115+ var mockOras = new Mock < IOrasService > ( ) ;
116+ mockOras
117+ . Setup ( o => o . GetReferrersAsync ( "myacr.azurecr.io/repo:tag" , It . IsAny < CancellationToken > ( ) ) )
118+ . ReturnsAsync ( [ "myacr.azurecr.io/repo@sha256:ref1" , "myacr.azurecr.io/repo@sha256:ref2" ] ) ;
119+
120+ var service = new CopyImageService (
121+ Mock . Of < ILogger < CopyImageService > > ( ) ,
122+ mockImporter . Object ,
123+ mockOras . Object ,
124+ ConfigurationHelper . CreateOptionsMock ( publishConfig ) ) ;
125+
126+ await service . ImportImageAsync (
127+ destTagNames : [ "mirror/repo:tag" ] ,
128+ destAcrName : "myacr.azurecr.io" ,
129+ srcTagName : "repo:tag" ,
130+ srcRegistryName : "myacr.azurecr.io" ,
131+ isDryRun : false ) ;
132+
133+ // Main image import with TargetTags
134+ mockImporter . Verify (
135+ x => x . ImportImageAsync (
136+ "myacr.azurecr.io" ,
137+ It . IsAny < ResourceIdentifier > ( ) ,
138+ It . Is < ContainerRegistryImportImageContent > ( c =>
139+ c . TargetTags . Count == 1 && c . TargetTags [ 0 ] == "mirror/repo:tag" ) ) ,
140+ Times . Once ) ;
141+
142+ // Two referrer imports with UntaggedTargetRepositories
143+ mockImporter . Verify (
144+ x => x . ImportImageAsync (
145+ "myacr.azurecr.io" ,
146+ It . IsAny < ResourceIdentifier > ( ) ,
147+ It . Is < ContainerRegistryImportImageContent > ( c =>
148+ c . UntaggedTargetRepositories . Count == 1
149+ && c . UntaggedTargetRepositories [ 0 ] == "mirror/repo"
150+ && c . Source . SourceImage == "repo@sha256:ref1" ) ) ,
151+ Times . Once ) ;
152+
153+ mockImporter . Verify (
154+ x => x . ImportImageAsync (
155+ "myacr.azurecr.io" ,
156+ It . IsAny < ResourceIdentifier > ( ) ,
157+ It . Is < ContainerRegistryImportImageContent > ( c =>
158+ c . UntaggedTargetRepositories . Count == 1
159+ && c . UntaggedTargetRepositories [ 0 ] == "mirror/repo"
160+ && c . Source . SourceImage == "repo@sha256:ref2" ) ) ,
161+ Times . Once ) ;
162+
163+ // Total: 1 main + 2 referrers = 3
164+ mockImporter . Verify (
165+ x => x . ImportImageAsync (
166+ It . IsAny < string > ( ) ,
167+ It . IsAny < ResourceIdentifier > ( ) ,
168+ It . IsAny < ContainerRegistryImportImageContent > ( ) ) ,
169+ Times . Exactly ( 3 ) ) ;
170+ }
171+
172+ /// <summary>
173+ /// When no referrers exist, ImportImageAsync should import only the main image.
174+ /// </summary>
175+ [ Fact ]
176+ public async Task ImportImageAsync_NoReferrers_ImportsOnlySourceImage ( )
177+ {
178+ PublishConfiguration publishConfig = CreateAcrPublishConfig ( "myacr.azurecr.io" ) ;
179+
180+ var mockImporter = new Mock < IAcrImageImporter > ( ) ;
181+ var mockOras = new Mock < IOrasService > ( ) ;
182+ mockOras
183+ . Setup ( o => o . GetReferrersAsync ( It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) )
184+ . ReturnsAsync ( Array . Empty < string > ( ) ) ;
185+
186+ var service = new CopyImageService (
187+ Mock . Of < ILogger < CopyImageService > > ( ) ,
188+ mockImporter . Object ,
189+ mockOras . Object ,
190+ ConfigurationHelper . CreateOptionsMock ( publishConfig ) ) ;
191+
192+ await service . ImportImageAsync (
193+ destTagNames : [ "mirror/repo:tag" ] ,
194+ destAcrName : "myacr.azurecr.io" ,
195+ srcTagName : "repo:tag" ,
196+ srcRegistryName : "myacr.azurecr.io" ,
197+ isDryRun : false ) ;
198+
199+ mockImporter . Verify (
200+ x => x . ImportImageAsync (
201+ It . IsAny < string > ( ) ,
202+ It . IsAny < ResourceIdentifier > ( ) ,
203+ It . IsAny < ContainerRegistryImportImageContent > ( ) ) ,
204+ Times . Once ) ;
205+ }
206+
207+ /// <summary>
208+ /// In dry-run mode, referrer discovery should be skipped entirely since it
209+ /// requires registry connectivity.
210+ /// </summary>
211+ [ Fact ]
212+ public async Task ImportImageAsync_DryRun_SkipsReferrerDiscovery ( )
213+ {
214+ var mockOras = new Mock < IOrasService > ( ) ;
215+
216+ var service = new CopyImageService (
217+ Mock . Of < ILogger < CopyImageService > > ( ) ,
218+ Mock . Of < IAcrImageImporter > ( ) ,
219+ mockOras . Object ,
220+ ConfigurationHelper . CreateOptionsMock ( new PublishConfiguration ( ) ) ) ;
221+
222+ await service . ImportImageAsync (
223+ destTagNames : [ "myacr.azurecr.io/repo:tag" ] ,
224+ destAcrName : "myacr.azurecr.io" ,
225+ srcTagName : "repo:tag" ,
226+ srcRegistryName : "myacr.azurecr.io" ,
227+ isDryRun : true ) ;
228+
229+ mockOras . Verify (
230+ o => o . GetReferrersAsync ( It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) ,
231+ Times . Never ) ;
232+ }
233+
234+ private static PublishConfiguration CreateAcrPublishConfig ( string acrServer )
235+ {
236+ return new PublishConfiguration
237+ {
238+ RegistryAuthentication =
239+ [
240+ new RegistryAuthentication
241+ {
242+ Server = acrServer ,
243+ ResourceGroup = "my-rg" ,
244+ Subscription = Guid . NewGuid ( ) . ToString ( ) ,
245+ ServiceConnection = new ServiceConnection
246+ {
247+ Name = "test" ,
248+ Id = Guid . NewGuid ( ) . ToString ( ) ,
249+ TenantId = Guid . NewGuid ( ) . ToString ( ) ,
250+ ClientId = Guid . NewGuid ( ) . ToString ( )
251+ }
252+ }
253+ ]
254+ } ;
255+ }
95256}
0 commit comments