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 }