@@ -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