TeraDurableMediaRootsTests.swift (7803B)
1 import CryptoKit 2 import Foundation 3 import RadrootsKit 4 @testable import TeraApp 5 import XCTest 6 7 final class TeraDurableMediaRootsTests: XCTestCase { 8 func testVerifiedLegacyCopySurvivesTemporaryAndCacheRemoval() throws { 9 let fixture = try DurableMediaFixture() 10 defer { fixture.remove() } 11 let roots = try TeraDurableMediaRoots.selectingStaging(in: fixture.legacy) 12 XCTAssertEqual(roots.stagedBlobsRoot, roots.dataRoot.appendingPathComponent("staged_blobs", isDirectory: true)) 13 try TeraDurableMediaRoots.restoreLegacyBlob(fixture.blob, roots: roots) 14 let access = RadrootsAppleFileAccess(roots: roots) 15 XCTAssertEqual(try access.readStagedBlob(fixture.blob), fixture.bytes) 16 XCTAssertEqual(try Data(contentsOf: fixture.legacyURL), fixture.bytes) 17 try FileManager.default.removeItem(at: roots.temporaryRoot) 18 try FileManager.default.removeItem(at: roots.cacheRoot) 19 try TeraDurableMediaRoots.restoreLegacyBlob(fixture.blob, roots: roots) 20 XCTAssertEqual(try access.readStagedBlob(fixture.blob), fixture.bytes) 21 } 22 23 func testCorruptAndSymlinkLegacySourcesCannotBecomeDurableMedia() throws { 24 for symlink in [false, true] { 25 let fixture = try DurableMediaFixture() 26 defer { fixture.remove() } 27 let roots = try TeraDurableMediaRoots.selectingStaging(in: fixture.legacy) 28 try FileManager.default.removeItem(at: fixture.legacyURL) 29 if symlink { 30 let outside = fixture.root.appendingPathComponent("outside") 31 try fixture.bytes.write(to: outside) 32 try FileManager.default.createSymbolicLink(at: fixture.legacyURL, withDestinationURL: outside) 33 } else { 34 try Data(repeating: 0, count: fixture.bytes.count).write(to: fixture.legacyURL) 35 } 36 XCTAssertThrowsError(try TeraDurableMediaRoots.restoreLegacyBlob(fixture.blob, roots: roots)) 37 XCTAssertFalse(try FileManager.default.fileExists(atPath: roots.stagedBlobURL(for: fixture.blob).path)) 38 } 39 } 40 41 func testNewWorkUsesForegroundWhileExistingNativeReceiptRemainsDiscoverable() async throws { 42 let fixture = try BackgroundUploadFixture() 43 defer { fixture.remove() } 44 let transfer = BackgroundTransferHarness() 45 let coordinator = fixture.coordinator(transfer: transfer) 46 let fresh = try await coordinator.prefersSharedForegroundUpload(ownerID: fixture.draftID) 47 XCTAssertTrue(fresh) 48 let request = try fixture.request(job: fixture.job(revision: 1, operation: String(repeating: "2", count: 32))) 49 try await transfer.seed(request: request, state: .awaitingVerification) 50 let retained = try await coordinator.prefersSharedForegroundUpload(ownerID: fixture.draftID) 51 XCTAssertFalse(retained) 52 let unrelated = try await coordinator.prefersSharedForegroundUpload(ownerID: String(repeating: "3", count: 32)) 53 XCTAssertTrue(unrelated) 54 try await transfer.setState(.completed) 55 let settled = try await coordinator.prefersSharedForegroundUpload(ownerID: fixture.draftID) 56 XCTAssertTrue(settled) 57 let count = await transfer.enqueueCount 58 XCTAssertEqual(count, 0) 59 } 60 61 func testConflictingDestinationIsRefusedAcrossRestartsWithoutReplacingEitherCopy() throws { 62 for symlink in [false, true] { 63 let fixture = try DurableMediaFixture() 64 defer { fixture.remove() } 65 let roots = try TeraDurableMediaRoots.selectingStaging(in: fixture.legacy) 66 let destination = try roots.stagedBlobURL(for: fixture.blob) 67 try FileManager.default.createDirectory(at: roots.stagedBlobsRoot, withIntermediateDirectories: true) 68 let conflict = Data(repeating: 0, count: fixture.bytes.count) 69 let outside = fixture.root.appendingPathComponent("unrelated") 70 if symlink { 71 try conflict.write(to: outside) 72 try FileManager.default.createSymbolicLink(at: destination, withDestinationURL: outside) 73 } else { 74 try conflict.write(to: destination) 75 } 76 for _ in 0 ..< 2 { 77 let reopened = try TeraDurableMediaRoots.selectingStaging(in: fixture.legacy) 78 XCTAssertThrowsError(try TeraDurableMediaRoots.restoreLegacyBlob(fixture.blob, roots: reopened)) { error in 79 if symlink { 80 XCTAssertEqual(error as? RadrootsAppleFileError, .permanentFailure) 81 } else { 82 XCTAssertEqual((error as? TeraRuntimeFailure)?.code, "ios.add.media_corrupt") 83 } 84 } 85 XCTAssertEqual(try Data(contentsOf: destination), conflict) 86 XCTAssertEqual(try Data(contentsOf: fixture.legacyURL), fixture.bytes) 87 if symlink { 88 XCTAssertEqual(try FileManager.default.destinationOfSymbolicLink(atPath: destination.path), outside.path) 89 } 90 } 91 } 92 } 93 94 func testPartialLegacySourceRemainsRecoverableWithoutPublishingAnEmptyReplacement() throws { 95 let fixture = try DurableMediaFixture() 96 defer { fixture.remove() } 97 let partial = Data(fixture.bytes.prefix(4)) 98 try partial.write(to: fixture.legacyURL) 99 for _ in 0 ..< 2 { 100 let roots = try TeraDurableMediaRoots.selectingStaging(in: fixture.legacy) 101 XCTAssertThrowsError(try TeraDurableMediaRoots.restoreLegacyBlob(fixture.blob, roots: roots)) 102 XCTAssertFalse(try FileManager.default.fileExists(atPath: roots.stagedBlobURL(for: fixture.blob).path)) 103 XCTAssertEqual(try Data(contentsOf: fixture.legacyURL), partial) 104 } 105 } 106 107 func testUnpublishedPartialFileIsRetainedAndPublishedCopyReconcilesOnRepeatedRestart() throws { 108 let fixture = try DurableMediaFixture() 109 defer { fixture.remove() } 110 let roots = try TeraDurableMediaRoots.selectingStaging(in: fixture.legacy) 111 try FileManager.default.createDirectory(at: roots.stagedBlobsRoot, withIntermediateDirectories: true) 112 let pending = roots.stagedBlobsRoot.appendingPathComponent(".radroots_pending_12345678-1234-1234-1234-123456789abc") 113 let partial = Data("unpublished partial bytes".utf8) 114 try partial.write(to: pending) 115 for _ in 0 ..< 3 { 116 let reopened = try TeraDurableMediaRoots.selectingStaging(in: fixture.legacy) 117 try TeraDurableMediaRoots.restoreLegacyBlob(fixture.blob, roots: reopened) 118 XCTAssertEqual(try RadrootsAppleFileAccess(roots: reopened).readStagedBlob(fixture.blob), fixture.bytes) 119 XCTAssertEqual(try Data(contentsOf: fixture.legacyURL), fixture.bytes) 120 XCTAssertEqual(try Data(contentsOf: pending), partial) 121 XCTAssertEqual(try Set(FileManager.default.contentsOfDirectory(atPath: roots.stagedBlobsRoot.path)), 122 [fixture.blob.blobID, pending.lastPathComponent]) 123 } 124 } 125 } 126 127 private struct DurableMediaFixture { 128 let root: URL 129 let legacy: RadrootsAppleFileRoots 130 let blob: RadrootsStagedBlobReference 131 let bytes = Data("acknowledged prepared bytes".utf8) 132 var legacyURL: URL { 133 legacy.stagedBlobsRoot.appendingPathComponent(blob.blobID) 134 } 135 136 init() throws { 137 root = FileManager.default.temporaryDirectory.appendingPathComponent("tera-durable-media-\(UUID().uuidString)", isDirectory: true) 138 legacy = try RadrootsAppleFileRoots( 139 appIdentifier: "test.tera.media", dataRoot: root.appendingPathComponent("data", isDirectory: true), 140 cacheRoot: root.appendingPathComponent("cache", isDirectory: true), 141 temporaryRoot: root.appendingPathComponent("temporary", isDirectory: true) 142 ) 143 let digest = SHA256.hash(data: bytes).map { String(format: "%02x", $0) }.joined() 144 blob = try RadrootsStagedBlobReference(blobID: digest, sizeBytes: bytes.count, mediaType: "image/png", filenameHint: "\(digest).png") 145 try FileManager.default.createDirectory(at: legacy.stagedBlobsRoot, withIntermediateDirectories: true) 146 try FileManager.default.createDirectory(at: legacy.cacheRoot, withIntermediateDirectories: true) 147 try bytes.write(to: legacyURL) 148 } 149 150 func remove() { 151 try? FileManager.default.removeItem(at: root) 152 } 153 }