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