field_ios

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

TeraMediaStoreTests.swift (9961B)


      1 @testable import TeraApp
      2 import XCTest
      3 
      4 final class TeraMediaStoreTests: XCTestCase {
      5     @MainActor
      6     func testVerifiedReferenceLoadsOnlyDigestBoundCachedBytes() async throws {
      7         let artifact = makeArtifact()
      8         let backend = MediaBackend(cachedArtifact: artifact)
      9         let client = try await startedClient(backend: backend)
     10         let store = TeraMediaStore(runtimeClient: client)
     11 
     12         store.load(media: makeReference(verification: .verified), context: makeContext())
     13 
     14         await waitUntil {
     15             store.state(for: makeReference(verification: .verified), context: makeContext())
     16                 == .ready(artifact)
     17         }
     18         let calls = await backend.callCounts()
     19         XCTAssertEqual(calls.cached, 1)
     20         XCTAssertEqual(calls.retrieved, 0)
     21         _ = try await client.stop()
     22     }
     23 
     24     @MainActor
     25     func testUnavailableReferenceRetrievesAndPresentsVerifiedBytes() async throws {
     26         let artifact = makeArtifact()
     27         let backend = MediaBackend(retrievedArtifact: artifact)
     28         let client = try await startedClient(backend: backend)
     29         let store = TeraMediaStore(runtimeClient: client)
     30         let reference = makeReference(verification: .unavailable)
     31 
     32         store.load(media: reference, context: makeContext())
     33 
     34         await waitUntil {
     35             store.state(for: reference, context: makeContext()) == .ready(artifact)
     36         }
     37         let calls = await backend.callCounts()
     38         XCTAssertEqual(calls.cached, 0)
     39         XCTAssertEqual(calls.retrieved, 1)
     40         _ = try await client.stop()
     41     }
     42 
     43     @MainActor
     44     func testTransportFailurePresentsServiceUnavailableState() async throws {
     45         let backend = MediaBackend(
     46             retrievalFailure: TeraRuntimeFailure(
     47               schemaVersion: 1,
     48               code: "blossom_transport_failed",
     49               category: "network",
     50               retryable: true,
     51               recoveryActions: ["retry"],
     52               operationID: "test.media.retrieve",
     53               capabilityID: "blossom_source",
     54               safeMessage: "The photo service is offline."
     55             )
     56         )
     57         let client = try await startedClient(backend: backend)
     58         let store = TeraMediaStore(runtimeClient: client)
     59         let reference = makeReference(verification: .unavailable)
     60 
     61         store.load(media: reference, context: makeContext())
     62 
     63         await waitUntil {
     64             store.state(for: reference, context: makeContext()) == .networkUnavailable
     65         }
     66         _ = try await client.stop()
     67     }
     68 
     69     @MainActor
     70     func testUndecodableArtifactIsInvalidatedAndPresentedAsCorrupt() async throws {
     71         let artifact = makeArtifact(bytes: Data([0x00, 0x01, 0x02]))
     72         let backend = MediaBackend(cachedArtifact: artifact)
     73         let client = try await startedClient(backend: backend)
     74         let store = TeraMediaStore(runtimeClient: client)
     75         let reference = makeReference(verification: .verified)
     76 
     77         store.load(media: reference, context: makeContext())
     78 
     79         await waitUntil {
     80             store.state(for: reference, context: makeContext()) == .corrupt
     81         }
     82         let invalidatedArtifactIDs = await backend.invalidatedArtifactIDs()
     83         XCTAssertEqual(invalidatedArtifactIDs, [artifact.artifactID])
     84         _ = try await client.stop()
     85     }
     86 
     87     @MainActor
     88     private func startedClient(backend: MediaBackend) async throws -> TeraRuntimeClient {
     89         let client = TeraRuntimeClient { _ in
     90             await TeraRuntimeBackendStart(backend: backend, snapshot: backend.snapshot())
     91         }
     92         _ = try await client.start(configuration: makeConfiguration())
     93         return client
     94     }
     95 
     96     @MainActor
     97     private func waitUntil(
     98       attempts: Int = 100,
     99       condition: () -> Bool
    100     ) async {
    101         for _ in 0 ..< attempts {
    102             if condition() {
    103                 return
    104             }
    105             await Task.yield()
    106             try? await Task.sleep(nanoseconds: 1_000_000)
    107         }
    108         XCTFail("Timed out waiting for media state transition")
    109     }
    110 
    111     private func makeConfiguration() -> TeraRuntimeLaunchConfiguration {
    112         TeraRuntimeLaunchConfiguration(
    113           applicationSupportDirectory: "/tmp/radroots-media-tests",
    114           publicKeyHex: String(repeating: "ab", count: 32),
    115           sourceGenerationHex: String(repeating: "cd", count: 32),
    116           sourceGenerationCreatedAtUnixMilliseconds: 1,
    117           protectedData: .available,
    118           networkProfile: .simulator,
    119           writableRelays: ["ws://127.0.0.1:21000"],
    120           blossom: TeraBlossomEndpointConfiguration(
    121             hostKind: .simulator,
    122             endpointAuthority: .loopbackDevelopment,
    123             primaryOrigin: "http://127.0.0.1:21100",
    124             fallbackOrigins: []
    125           ),
    126           app: TeraRuntimeAppMetadata(
    127             bundleIdentifier: "org.radroots.media-tests",
    128             version: "0.1.0-alpha",
    129             buildNumber: "1",
    130             buildSHA: nil
    131           ),
    132           signerGeneration: "media-tests",
    133           signer: MediaTestSigner(),
    134           adoptBootstrapSettings: false
    135         )
    136     }
    137 
    138     private func makeContext() -> TeraLocalNetwork {
    139         TeraLocalNetwork(
    140           schemaVersion: 1,
    141           id: "media-context",
    142           label: "Media context",
    143           relayURLs: ["ws://127.0.0.1:21000"],
    144           locality: nil,
    145           followedAuthors: [],
    146           generation: 1
    147         )
    148     }
    149 
    150     private func makeReference(
    151         verification: TeraMediaVerificationState
    152     ) -> TeraMediaReference {
    153         TeraMediaReference(
    154           referenceFingerprint: String(repeating: "f", count: 64),
    155           url: "https://blossom.example/\(String(repeating: "a", count: 64)).png",
    156           sha256: String(repeating: "a", count: 64),
    157           mediaType: "image/png",
    158           width: 1,
    159           height: 1,
    160           byteSize: nil,
    161           alt: "A farm photo",
    162           verification: verification
    163         )
    164     }
    165 
    166     private func makeArtifact(
    167         bytes: Data = Data(
    168             base64Encoded: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZJbIAAAAASUVORK5CYII="
    169         )!
    170     ) -> TeraVerifiedMediaArtifact {
    171         TeraVerifiedMediaArtifact(
    172           artifactID: String(repeating: "a", count: 64),
    173           bytes: bytes,
    174           byteSize: UInt64(bytes.count),
    175           mediaType: "image/png",
    176           width: 1,
    177           height: 1
    178         )!
    179     }
    180 }
    181 
    182 private struct MediaTestSigner: TeraRuntimeSigner {
    183     func availability() async -> TeraRuntimeSignerAvailability {
    184         .ready
    185     }
    186 
    187     func sign(_: TeraRuntimeSigningRequest) async -> TeraRuntimeSigningOutcome {
    188         .failed
    189     }
    190 }
    191 
    192 private actor MediaBackend: TeraRuntimeBackend {
    193     private var cachedArtifact: TeraVerifiedMediaArtifact?
    194     private let retrievedArtifact: TeraVerifiedMediaArtifact?
    195     private let retrievalFailure: TeraRuntimeFailure?
    196     private var cachedCalls = 0
    197     private var retrieveCalls = 0
    198     private var invalidatedIDs: [String] = []
    199     private var closed = false
    200 
    201     init(
    202       cachedArtifact: TeraVerifiedMediaArtifact? = nil,
    203       retrievedArtifact: TeraVerifiedMediaArtifact? = nil,
    204       retrievalFailure: TeraRuntimeFailure? = nil
    205     ) {
    206         self.cachedArtifact = cachedArtifact
    207         self.retrievedArtifact = retrievedArtifact
    208         self.retrievalFailure = retrievalFailure
    209     }
    210 
    211     func snapshot() -> TeraRuntimeSnapshot {
    212         TeraRuntimeSnapshot(
    213           identity: TeraRuntimeIdentity(
    214             publicKeyHex: String(repeating: "ab", count: 32),
    215             hostSignerConfigured: true
    216           ),
    217           relay: nil,
    218           blossomConfiguration: nil,
    219           blossomEvidence: nil,
    220           crateName: "tera_ffi",
    221           crateVersion: "0.1.0-alpha",
    222           isClosed: closed
    223         )
    224     }
    225 
    226     func todayPage(request _: TeraTodayPageRequest) throws -> TeraTodayPage {
    227         throw testFailure(operation: "test.media.today")
    228     }
    229 
    230     func refreshToday(
    231       context _: TeraLocalNetwork,
    232       nowUnixSeconds _: UInt64,
    233       update _: TeraTodayProjectionUpdate,
    234       backfillCursor _: String?
    235     ) throws -> TeraTodaySyncReceipt {
    236         throw testFailure(operation: "test.media.refresh")
    237     }
    238 
    239     func retrieveMedia(
    240       context _: TeraLocalNetwork,
    241       reference _: TeraMediaReference
    242     ) throws -> TeraVerifiedMediaArtifact {
    243         retrieveCalls += 1
    244         if let retrievalFailure {
    245             throw retrievalFailure
    246         }
    247         guard let retrievedArtifact else {
    248             throw testFailure(operation: "test.media.retrieve")
    249         }
    250         return retrievedArtifact
    251     }
    252 
    253     func verifiedMediaArtifact(
    254       context _: TeraLocalNetwork,
    255       artifactID _: String
    256     ) -> TeraVerifiedMediaArtifact? {
    257         cachedCalls += 1
    258         return cachedArtifact
    259     }
    260 
    261     func invalidateMediaArtifact(
    262       context _: TeraLocalNetwork,
    263       artifactID: String
    264     ) -> Bool {
    265         invalidatedIDs.append(artifactID)
    266         cachedArtifact = nil
    267         return true
    268     }
    269 
    270     func subscribe(
    271       bufferCapacity _: Int,
    272       receive _: @escaping @Sendable (TeraRuntimeChange) async -> Void
    273     ) -> any TeraRuntimeSubscriptionToken {
    274         MediaSubscriptionToken()
    275     }
    276 
    277     func shutdown() -> TeraRuntimeShutdownReceipt {
    278         let alreadyClosed = closed
    279         closed = true
    280         return TeraRuntimeShutdownReceipt(state: "closed", alreadyClosed: alreadyClosed)
    281     }
    282 
    283     func callCounts() -> (cached: Int, retrieved: Int) {
    284         (cachedCalls, retrieveCalls)
    285     }
    286 
    287     func invalidatedArtifactIDs() -> [String] {
    288         invalidatedIDs
    289     }
    290 
    291     private func testFailure(operation: String) -> TeraRuntimeFailure {
    292         .local(
    293           operation: operation,
    294           code: "test.media.unsupported",
    295           safeMessage: "This test operation is unsupported."
    296         )
    297     }
    298 }
    299 
    300 private actor MediaSubscriptionToken: TeraRuntimeSubscriptionToken {
    301     func cancel() {}
    302 }