field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

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 }