TeraMediaOwnershipFFITests.swift (8108B)
1 import CryptoKit 2 import Darwin 3 import Foundation 4 @testable import TeraApp 5 import TeraKitBindings 6 import XCTest 7 8 final class TeraMediaOwnershipFFITests: XCTestCase { 9 func testCallerCloseBeforeGeneratedConversionPreservesAdmittedBytes() async throws { 10 let fixture = try MediaOwnershipFixture() 11 defer { fixture.remove() } 12 let runtime = try await fixture.runtime() 13 let opened = try TeraMediaFileFixture.open([fixture.media], bytes: fixture.bytes) 14 opened.close() 15 let saved = try await fixture.save(runtime, handle: XCTUnwrap(opened.handles.first)) 16 XCTAssertEqual(saved.form?.media.first?.sha256, fixture.media.sha256) 17 _ = try await runtime.shutdown() 18 } 19 20 func testReusingCallerDescriptorBeforeGeneratedConversionCannotSubstituteBytes() async throws { 21 let fixture = try MediaOwnershipFixture() 22 defer { fixture.remove() } 23 let runtime = try await fixture.runtime() 24 let original = try fixture.original() 25 defer { try? original.close() } 26 let handle = try TeraPreparedMediaHandle(media: fixture.media, fileDescriptor: UInt64(original.fileDescriptor)) 27 let replacementURL = fixture.root.appendingPathComponent("replacement") 28 try Data(repeating: 0, count: fixture.bytes.count).write(to: replacementURL) 29 let replacement = try FileHandle(forReadingFrom: replacementURL) 30 defer { try? replacement.close() } 31 // Both descriptor slots belong exclusively to this fixture. The original 32 // FileHandle remains the sole closer of the atomically replaced slot. 33 XCTAssertEqual(dup2(replacement.fileDescriptor, original.fileDescriptor), original.fileDescriptor) 34 XCTAssertEqual(try original.readToEnd(), Data(repeating: 0, count: fixture.bytes.count)) 35 let saved = try await fixture.save(runtime, handle: handle) 36 XCTAssertEqual(saved.form?.media.first?.sha256, fixture.media.sha256) 37 _ = try await runtime.shutdown() 38 } 39 40 func testCancelledBoundedCallerCanCloseWhileLateForeignWorkRetainsItsFile() async throws { 41 try await assertLateWork(timeout: false) 42 } 43 44 func testTimedOutBoundedCallerCanCloseWhileLateForeignWorkRetainsItsFile() async throws { 45 try await assertLateWork(timeout: true) 46 } 47 48 func testAdmissionRejectsInvalidAndNonregularDescriptorsSynchronously() throws { 49 let fixture = try MediaOwnershipFixture() 50 defer { fixture.remove() } 51 XCTAssertThrowsError(try TeraPreparedMediaHandle(media: fixture.media, fileDescriptor: .max)) 52 let directory = Darwin.open(fixture.root.path, O_RDONLY) 53 XCTAssertGreaterThanOrEqual(directory, 0) 54 guard directory >= 0 else { return } 55 defer { Darwin.close(directory) } 56 XCTAssertThrowsError(try TeraPreparedMediaHandle(media: fixture.media, fileDescriptor: UInt64(directory))) 57 } 58 59 func testPreparedMediaSchemaIsVersionedWithoutChangingDraftSchema() async throws { 60 let fixture = try MediaOwnershipFixture() 61 defer { fixture.remove() } 62 let runtime = try await fixture.runtime() 63 let opened = try TeraMediaFileFixture.open([fixture.media], bytes: fixture.bytes) 64 let handle = try XCTUnwrap(opened.handles.first) 65 var input = fixture.input(handle) 66 XCTAssertEqual(input.schemaVersion, 1) 67 XCTAssertEqual(input.media[0].schemaVersion, 2) 68 input.media[0].schemaVersion = 1 69 do { 70 _ = try await runtime.phase1SaveDraft( 71 draftId: String(repeating: "31", count: 16), input: input, 72 authoredAtUnixS: 1_800_000_000, expectedRevision: nil, persistedAtUnixMs: 1_800_000_000_000 73 ) 74 XCTFail("The borrowed media schema must be rejected") 75 } catch let TeraAppError.Failure(report) { 76 XCTAssertEqual(report.code, "unsupported_schema_version") 77 } 78 _ = try await runtime.shutdown() 79 } 80 81 private func assertLateWork(timeout: Bool) async throws { 82 let fixture = try MediaOwnershipFixture() 83 defer { fixture.remove() } 84 let runtime = try await fixture.runtime() 85 let original = try fixture.original() 86 let handle = try TeraPreparedMediaHandle(media: fixture.media, fileDescriptor: UInt64(original.fileDescriptor)) 87 let opened = TeraOpenedMedia(handles: [handle], files: [original]) 88 let pause = ResourceTestPause() 89 let task = TeraRuntimeBoundedTask<String>(deadlineNanoseconds: timeout ? 1 : .max) { 90 await pause.wait() 91 // Model independently executing foreign work: caller cancellation does 92 // not establish that the operation stopped or that its resource is free. 93 return await Task.detached { () -> Result<String, TeraRuntimeFailure> in 94 do { 95 let saved = try await fixture.save(runtime, handle: handle) 96 return .success(saved.form?.media.first?.sha256 ?? "missing") 97 } catch { 98 return .failure(TeraRuntimeFailure.local(operation: "fixture", code: "fixture.media.failed", safeMessage: "Fixture failed.")) 99 } 100 }.value 101 } 102 await pause.entered.wait() 103 if !timeout { 104 task.cancel() 105 } 106 switch await task.value() { 107 case .timedOut: XCTAssertTrue(timeout) 108 case .cancelled: XCTAssertFalse(timeout) 109 case .completed: XCTFail("The foreign work is still paused") 110 } 111 await TeraOpenedMediaCloseFixture.closeConcurrently(opened) 112 XCTAssertNil(task.settlement()) 113 await pause.resume.open() 114 let late = await task.settle() 115 XCTAssertEqual(try late.get(), fixture.media.sha256) 116 _ = try await runtime.shutdown() 117 } 118 } 119 120 struct MediaOwnershipFixture: Sendable { 121 let root: URL 122 let bytes = Data([137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 2, 0, 0, 0, 2]) 123 private let publicKey = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" 124 125 init() throws { 126 root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) 127 try FileManager.default.createDirectory(at: root.appendingPathComponent("radroots/users/\(publicKey)"), withIntermediateDirectories: true) 128 try bytes.write(to: root.appendingPathComponent("original.png")) 129 } 130 131 var media: TeraPreparedMedia { 132 let hash = SHA256.hash(data: bytes).map { String(format: "%02x", $0) }.joined() 133 return TeraPreparedMedia(opaqueReference: "media:\(hash)", remoteURL: nil, sha256: hash, 134 mediaType: "image/png", byteSize: UInt64(bytes.count), width: 2, height: 2, 135 alt: "Synthetic ownership fixture", preparedAtUnixSeconds: 1_800_000_000) 136 } 137 138 func original() throws -> FileHandle { 139 try FileHandle(forReadingFrom: root.appendingPathComponent("original.png")) 140 } 141 142 func remove() { 143 try? FileManager.default.removeItem(at: root) 144 } 145 146 func runtime() async throws -> TeraRuntime { 147 let runtime = try await TeraRuntime(applicationSupportDirectory: root.path, publicKeyHex: publicKey, 148 sourceGenerationHex: String(repeating: "04", count: 32), 149 sourceGenerationCreatedAtUnixMs: 1_800_000_000_000, protectedData: .available) 150 try await runtime.configureBlossom(hostKind: .simulator, endpointAuthority: .loopbackDevelopment, 151 primaryOrigin: "http://127.0.0.1:3000", fallbackOrigins: []) 152 return runtime 153 } 154 155 func save(_ runtime: TeraRuntime, handle: TeraPreparedMediaHandle) async throws -> FfiDraftStatusRecord { 156 try await runtime.phase1SaveDraft(draftId: String(repeating: "31", count: 16), input: input(handle), 157 authoredAtUnixS: 1_800_000_000, expectedRevision: nil, persistedAtUnixMs: 1_800_000_000_000) 158 } 159 160 func input(_ handle: TeraPreparedMediaHandle) -> FfiAddDraftInput { 161 FfiAddDraftInput(schemaVersion: 1, commandType: .createPhotoUpdate, content: "Synthetic ownership fixture", 162 identifier: nil, title: nil, summary: nil, location: nil, eventTiming: nil, 163 eventStartDate: nil, eventEndDate: nil, eventStartUnixS: nil, eventEndUnixS: nil, 164 eventTimezone: nil, priceAmount: nil, currency: nil, unit: nil, quantity: nil, 165 foodPublishedAtUnixS: nil, foodStatus: nil, media: [handle.generatedValue]) 166 } 167 }