@@ -159,113 +159,125 @@ struct OCIClientTests: ~Copyable {
159
159
#expect( done)
160
160
}
161
161
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
+ )
185
174
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
+ )
191
189
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
+ )
199
205
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
+ )
203
212
204
- let name = " apple/ test-images /image-push "
213
+ let name = " test/image "
205
214
let ref = " latest "
206
215
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
221
217
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
+ )
238
227
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
246
236
)
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 (
249
241
name: name,
250
242
ref: ref,
251
- content: newManifest,
252
- baseDescriptor: manifestDescriptor!
243
+ descriptor: manifestDescriptor,
244
+ streamGenerator: { manifestStream } ,
245
+ progress: nil
253
246
)
254
247
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)
261
255
)
262
- try await self . pushDescriptor (
263
- client: client,
256
+
257
+ let indexStream = TestByteBufferSequence ( data: indexData)
258
+ try await mockClient. push (
264
259
name: name,
265
260
ref: ref,
266
- content: newIndex,
267
- baseDescriptor: indexDescriptor
261
+ descriptor: indexDescriptor,
262
+ streamGenerator: { indexStream } ,
263
+ progress: nil
268
264
)
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)
269
281
}
270
282
271
283
@Test func resolveWithRetry( ) async throws {
@@ -356,4 +368,143 @@ extension SHA256.Digest {
356
368
let parts = self . description. split ( separator: " : " )
357
369
return " sha256: \( parts [ 1 ] ) "
358
370
}
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
+ }
359
510
}
0 commit comments