@@ -159,113 +159,125 @@ struct OCIClientTests: ~Copyable {
159159        #expect( done) 
160160    } 
161161
162-     @Test ( . disabled( " External users cannot push images, disable while we find a better solution " ) )  
163-     func  pushIndex( )  async  throws  { 
164-         let  client  =  RegistryClient ( host:  " ghcr.io " ,  authentication:  Self . authentication) 
165-         let  indexDescriptor  =  try   await  client. resolve ( name:  " apple/containerization/emptyimage " ,  tag:  " 0.0.1 " ) 
166-         let  index :  Index  =  try   await  client. fetch ( name:  " apple/containerization/emptyimage " ,  descriptor:  indexDescriptor) 
167- 
168-         let  platform  =  Platform ( arch:  " amd64 " ,  os:  " linux " ) 
169- 
170-         var  manifestDescriptor :  Descriptor ? 
171-         for  m  in  index. manifests where  m. platform ==  platform { 
172-             manifestDescriptor =  m
173-             break 
174-         } 
175- 
176-         #expect( manifestDescriptor !=  nil ) 
177- 
178-         let  manifest :  Manifest  =  try   await  client. fetch ( name:  " apple/containerization/emptyimage " ,  descriptor:  manifestDescriptor!) 
179-         let  imgConfig :  Image  =  try   await  client. fetch ( name:  " apple/containerization/emptyimage " ,  descriptor:  manifest. config) 
180- 
181-         let  layer  =  try   #require( manifest. layers. first) 
182-         let  blobPath  =  contentPath. appendingPathComponent ( layer. digest) 
183-         let  outputStream  =  OutputStream ( toFileAtPath:  blobPath. path,  append:  false ) 
184-         #expect( outputStream !=  nil ) 
162+     @Test   func  pushIndexWithMock( )  async  throws  { 
163+         // Create a mock client for testing push operations
164+         let  mockClient  =  MockRegistryClient ( ) 
165+ 
166+         // Create test data for an index and its components
167+         let  testLayerData  =  " test layer content " . data ( using:  . utf8) !
168+         let  layerDigest  =  SHA256 . hash ( data:  testLayerData) 
169+         let  layerDescriptor  =  Descriptor ( 
170+             mediaType:  " application/vnd.docker.image.rootfs.diff.tar.gzip " , 
171+             digest:  " sha256: \( layerDigest. hexString) " , 
172+             size:  Int64 ( testLayerData. count) 
173+         ) 
185174
186-         try   await  outputStream!. withThrowingOpeningStream  { 
187-             try   await  client. fetchBlob ( name:  " apple/containerization/emptyimage " ,  descriptor:  layer)  {  ( expected,  body)  in 
188-                 var  received :  Int64  =  0 
189-                 for  try   await  buffer  in  body { 
190-                     received +=  Int64 ( buffer. readableBytes) 
175+         // Create test image config
176+         let  imageConfig  =  Image ( 
177+             architecture:  " amd64 " , 
178+             os:  " linux " , 
179+             config:  Image . Config ( labels:  [ " test " :  " value " ] ) , 
180+             rootfs:  Image . Rootfs ( type:  " layers " ,  diffIDs:  [ " sha256: \( layerDigest. hexString) " ] ) 
181+         ) 
182+         let  configData  =  try   JSONEncoder ( ) . encode ( imageConfig) 
183+         let  configDigest  =  SHA256 . hash ( data:  configData) 
184+         let  configDescriptor  =  Descriptor ( 
185+             mediaType:  " application/vnd.docker.container.image.v1+json " , 
186+             digest:  " sha256: \( configDigest. hexString) " , 
187+             size:  Int64 ( configData. count) 
188+         ) 
191189
192-                     buffer. withUnsafeReadableBytes  {  pointer in 
193-                         let  unsafeBufferPointer  =  pointer. bindMemory ( to:  UInt8 . self) 
194-                         if  let  addr =  unsafeBufferPointer. baseAddress { 
195-                             outputStream!. write ( addr,  maxLength:  buffer. readableBytes) 
196-                         } 
197-                     } 
198-                 } 
190+         // Create test manifest
191+         let  manifest  =  Manifest ( 
192+             schemaVersion:  2 , 
193+             mediaType:  " application/vnd.docker.distribution.manifest.v2+json " , 
194+             config:  configDescriptor, 
195+             layers:  [ layerDescriptor] 
196+         ) 
197+         let  manifestData  =  try   JSONEncoder ( ) . encode ( manifest) 
198+         let  manifestDigest  =  SHA256 . hash ( data:  manifestData) 
199+         let  manifestDescriptor  =  Descriptor ( 
200+             mediaType:  " application/vnd.docker.distribution.manifest.v2+json " , 
201+             digest:  " sha256: \( manifestDigest. hexString) " , 
202+             size:  Int64 ( manifestData. count) , 
203+             platform:  Platform ( arch:  " amd64 " ,  os:  " linux " ) 
204+         ) 
199205
200-                 #expect( received ==  expected) 
201-             } 
202-         } 
206+         // Create test index
207+         let  index  =  Index ( 
208+             schemaVersion:  2 , 
209+             mediaType:  " application/vnd.docker.distribution.manifest.list.v2+json " , 
210+             manifests:  [ manifestDescriptor] 
211+         ) 
203212
204-         let  name  =  " apple/ test-images /image-push " 
213+         let  name  =  " test/image " 
205214        let  ref  =  " latest " 
206215
207-         // Push the layer first.
208-         do  { 
209-             let  content  =  try   LocalContent ( path:  blobPath) 
210-             let  generator  =  { 
211-                 let  stream  =  try   ReadStream ( url:  content. path) 
212-                 try   stream. reset ( ) 
213-                 return  stream. stream
214-             } 
215-             try   await  client. push ( name:  name,  ref:  ref,  descriptor:  layer,  streamGenerator:  generator,  progress:  nil ) 
216-         }  catch  let  err  as ContainerizationError  { 
217-             guard  err. code ==  . exists else  { 
218-                 throw  err
219-             } 
220-         } 
216+         // Test pushing individual components using the mock client
221217
222-         // Push the image configuration.
223-         var  imgConfigDesc :  Descriptor ? 
224-         do  { 
225-             imgConfigDesc =  try   await  self . pushDescriptor ( 
226-                 client:  client, 
227-                 name:  name, 
228-                 ref:  ref, 
229-                 content:  imgConfig, 
230-                 baseDescriptor:  manifest. config
231-             ) 
232-         }  catch  let  err  as ContainerizationError  { 
233-             guard  err. code !=  . exists else  { 
234-                 return 
235-             } 
236-             throw  err
237-         } 
218+         // Push layer
219+         let  layerStream  =  TestByteBufferSequence ( data:  testLayerData) 
220+         try   await  mockClient. push ( 
221+             name:  name, 
222+             ref:  ref, 
223+             descriptor:  layerDescriptor, 
224+             streamGenerator:  {  layerStream } , 
225+             progress:  nil 
226+         ) 
238227
239-         // Push the image manifest.
240-         let  newManifest  =  Manifest ( 
241-             schemaVersion:  manifest. schemaVersion, 
242-             mediaType:  manifest. mediaType!, 
243-             config:  imgConfigDesc!, 
244-             layers:  manifest. layers, 
245-             annotations:  manifest. annotations
228+         // Push config
229+         let  configStream  =  TestByteBufferSequence ( data:  configData) 
230+         try   await  mockClient. push ( 
231+             name:  name, 
232+             ref:  ref, 
233+             descriptor:  configDescriptor, 
234+             streamGenerator:  {  configStream } , 
235+             progress:  nil 
246236        ) 
247-         let  manifestDesc  =  try   await  self . pushDescriptor ( 
248-             client:  client, 
237+ 
238+         // Push manifest
239+         let  manifestStream  =  TestByteBufferSequence ( data:  manifestData) 
240+         try   await  mockClient. push ( 
249241            name:  name, 
250242            ref:  ref, 
251-             content:  newManifest, 
252-             baseDescriptor:  manifestDescriptor!
243+             descriptor:  manifestDescriptor, 
244+             streamGenerator:  {  manifestStream } , 
245+             progress:  nil 
253246        ) 
254247
255-         // Push the index.
256-         let  newIndex  =  Index ( 
257-             schemaVersion:  index. schemaVersion, 
258-             mediaType:  index. mediaType, 
259-             manifests:  [ manifestDesc] , 
260-             annotations:  index. annotations
248+         // Push index
249+         let  indexData  =  try   JSONEncoder ( ) . encode ( index) 
250+         let  indexDigest  =  SHA256 . hash ( data:  indexData) 
251+         let  indexDescriptor  =  Descriptor ( 
252+             mediaType:  " application/vnd.docker.distribution.manifest.list.v2+json " , 
253+             digest:  " sha256: \( indexDigest. hexString) " , 
254+             size:  Int64 ( indexData. count) 
261255        ) 
262-         try   await  self . pushDescriptor ( 
263-             client:  client, 
256+ 
257+         let  indexStream  =  TestByteBufferSequence ( data:  indexData) 
258+         try   await  mockClient. push ( 
264259            name:  name, 
265260            ref:  ref, 
266-             content:  newIndex, 
267-             baseDescriptor:  indexDescriptor
261+             descriptor:  indexDescriptor, 
262+             streamGenerator:  {  indexStream } , 
263+             progress:  nil 
268264        ) 
265+ 
266+         // Verify all push operations were recorded
267+         #expect( mockClient. pushCalls. count ==  4 ) 
268+ 
269+         // Verify content integrity
270+         let  storedLayerData  =  mockClient. getPushedContent ( name:  name,  descriptor:  layerDescriptor) 
271+         #expect( storedLayerData ==  testLayerData) 
272+ 
273+         let  storedConfigData  =  mockClient. getPushedContent ( name:  name,  descriptor:  configDescriptor) 
274+         #expect( storedConfigData ==  configData) 
275+ 
276+         let  storedManifestData  =  mockClient. getPushedContent ( name:  name,  descriptor:  manifestDescriptor) 
277+         #expect( storedManifestData ==  manifestData) 
278+ 
279+         let  storedIndexData  =  mockClient. getPushedContent ( name:  name,  descriptor:  indexDescriptor) 
280+         #expect( storedIndexData ==  indexData) 
269281    } 
270282
271283    @Test   func  resolveWithRetry( )  async  throws  { 
@@ -356,4 +368,143 @@ extension SHA256.Digest {
356368        let  parts  =  self . description. split ( separator:  " :  " ) 
357369        return  " sha256: \( parts [ 1 ] ) " 
358370    } 
371+ 
372+     var  hexString :  String  { 
373+         self . compactMap  {  String ( format:  " %02x " ,  $0)  } . joined ( ) 
374+     } 
375+ } 
376+ 
377+ // Helper to create ByteBuffer sequences for testing
378+ struct  TestByteBufferSequence :  Sendable ,  AsyncSequence  { 
379+     typealias  Element  =  ByteBuffer 
380+ 
381+     private  let  data :  Data 
382+ 
383+     init ( data:  Data )  { 
384+         self . data =  data
385+     } 
386+ 
387+     func  makeAsyncIterator( )  ->  AsyncIterator  { 
388+         AsyncIterator ( data:  data) 
389+     } 
390+ 
391+     struct  AsyncIterator :  AsyncIteratorProtocol  { 
392+         private  let  data :  Data 
393+         private  var  sent   =  false 
394+ 
395+         init ( data:  Data )  { 
396+             self . data =  data
397+         } 
398+ 
399+         mutating  func  next( )  async  throws  ->  ByteBuffer ?   { 
400+             guard  !sent else  {  return  nil  } 
401+             sent =  true 
402+ 
403+             var  buffer  =  ByteBufferAllocator ( ) . buffer ( capacity:  data. count) 
404+             buffer. writeBytes ( data) 
405+             return  buffer
406+         } 
407+     } 
408+ } 
409+ 
410+ // Helper class to create a mock ContentClient for testing
411+ final  class  MockRegistryClient :  ContentClient  { 
412+     private  var  pushedContent :  [ String :  [ Descriptor :  Data ] ]  =  [ : ] 
413+     private  var  fetchableContent :  [ String :  [ Descriptor :  Data ] ]  =  [ : ] 
414+ 
415+     // Track push operations for verification
416+     var  pushCalls :  [ ( name:  String ,  ref:  String ,  descriptor:  Descriptor ) ]  =  [ ] 
417+ 
418+     func  addFetchableContent< T:  Codable > ( name:  String ,  descriptor:  Descriptor ,  content:  T )  throws  { 
419+         let  data  =  try   JSONEncoder ( ) . encode ( content) 
420+         if  fetchableContent [ name]  ==  nil  { 
421+             fetchableContent [ name]  =  [ : ] 
422+         } 
423+         fetchableContent [ name] ![ descriptor]  =  data
424+     } 
425+ 
426+     func  addFetchableData( name:  String ,  descriptor:  Descriptor ,  data:  Data )  { 
427+         if  fetchableContent [ name]  ==  nil  { 
428+             fetchableContent [ name]  =  [ : ] 
429+         } 
430+         fetchableContent [ name] ![ descriptor]  =  data
431+     } 
432+ 
433+     func  getPushedContent( name:  String ,  descriptor:  Descriptor )  ->  Data ?   { 
434+         pushedContent [ name] ? [ descriptor] 
435+     } 
436+ 
437+     // MARK: - ContentClient Implementation
438+ 
439+     func  fetch< T:  Codable > ( name:  String ,  descriptor:  Descriptor )  async  throws  ->  T  { 
440+         guard  let  imageContent =  fetchableContent [ name] , 
441+             let  data =  imageContent [ descriptor] 
442+         else  { 
443+             throw  ContainerizationError ( . notFound,  message:  " Content not found for  \( name)  with descriptor  \( descriptor. digest) " ) 
444+         } 
445+ 
446+         return  try   JSONDecoder ( ) . decode ( T . self,  from:  data) 
447+     } 
448+ 
449+     func  fetchBlob( name:  String ,  descriptor:  Descriptor ,  into file:  URL ,  progress:  ProgressHandler ? )  async  throws  ->  ( Int64 ,  SHA256Digest )  { 
450+         guard  let  imageContent =  fetchableContent [ name] , 
451+             let  data =  imageContent [ descriptor] 
452+         else  { 
453+             throw  ContainerizationError ( . notFound,  message:  " Blob not found for  \( name)  with descriptor  \( descriptor. digest) " ) 
454+         } 
455+ 
456+         try   data. write ( to:  file) 
457+         let  digest  =  SHA256 . hash ( data:  data) 
458+         return  ( Int64 ( data. count) ,  SHA256Digest ( digest:  digest. hexString) ) 
459+     } 
460+ 
461+     func  fetchData( name:  String ,  descriptor:  Descriptor )  async  throws  ->  Data  { 
462+         guard  let  imageContent =  fetchableContent [ name] , 
463+             let  data =  imageContent [ descriptor] 
464+         else  { 
465+             throw  ContainerizationError ( . notFound,  message:  " Data not found for  \( name)  with descriptor  \( descriptor. digest) " ) 
466+         } 
467+ 
468+         return  data
469+     } 
470+ 
471+     func  push< T:  Sendable  &  AsyncSequence > ( 
472+         name:  String , 
473+         ref:  String , 
474+         descriptor:  Descriptor , 
475+         streamGenerator:  ( )  throws  ->  T , 
476+         progress:  ProgressHandler ? 
477+     )  async  throws  where  T. Element ==  ByteBuffer  { 
478+         // Record the push call for verification
479+         pushCalls. append ( ( name:  name,  ref:  ref,  descriptor:  descriptor) ) 
480+ 
481+         // Simulate reading the stream and storing the data
482+         let  stream  =  try   streamGenerator ( ) 
483+         var  data  =  Data ( ) 
484+ 
485+         for  try   await  buffer  in  stream { 
486+             data. append ( contentsOf:  buffer. readableBytesView) 
487+         } 
488+ 
489+         // Verify the pushed data matches the expected descriptor
490+         let  actualDigest  =  SHA256 . hash ( data:  data) 
491+         guard  descriptor. digest ==  " sha256: \( actualDigest. hexString) "  else  { 
492+             throw  ContainerizationError ( . invalidArgument,  message:  " Digest mismatch: expected  \( descriptor. digest) , got sha256: \( actualDigest. hexString) " ) 
493+         } 
494+ 
495+         guard  data. count ==  descriptor. size else  { 
496+             throw  ContainerizationError ( . invalidArgument,  message:  " Size mismatch: expected  \( descriptor. size) , got  \( data. count) " ) 
497+         } 
498+ 
499+         // Store the pushed content
500+         if  pushedContent [ name]  ==  nil  { 
501+             pushedContent [ name]  =  [ : ] 
502+         } 
503+         pushedContent [ name] ![ descriptor]  =  data
504+ 
505+         // Simulate progress reporting
506+         if  let  progress =  progress { 
507+             await  progress ( Int64 ( data. count) ,  Int64 ( data. count) ) 
508+         } 
509+     } 
359510} 
0 commit comments