mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-12 05:29:57 +02:00
feat(apple): exercise saved-device core contract harness
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
167
apple/Tests/SavedDeviceCoreContractTests.swift
Normal file
167
apple/Tests/SavedDeviceCoreContractTests.swift
Normal file
@@ -0,0 +1,167 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
@preconcurrency import VnidropCore
|
||||
@testable import VniDrop
|
||||
|
||||
/// Headless Apple harness for the saved-device core/platform contract (ticket 14).
|
||||
///
|
||||
/// Exercises protected identity restart, event revision recovery, and binding
|
||||
/// hygiene against the generated UniFFI surface. The full two-node public-API
|
||||
/// lifecycle (eligibility → unblock) lives in
|
||||
/// `crates/vnidrop/src/tests/platform_contract_apple.rs` so it can run without
|
||||
/// the iOS simulator.
|
||||
final class SavedDeviceCoreContractTests: XCTestCase {
|
||||
private final class RecordingSink: CoreEventSink, @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var events: [CoreEvent] = []
|
||||
|
||||
func onEvent(event: CoreEvent) {
|
||||
lock.lock()
|
||||
events.append(event)
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func snapshot() -> [CoreEvent] {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return events
|
||||
}
|
||||
}
|
||||
|
||||
func testExperimentalKeychainIdentitySurvivesRestart() throws {
|
||||
let directory = try FileManager.default.url(
|
||||
for: .itemReplacementDirectory,
|
||||
in: .userDomainMask,
|
||||
appropriateFor: FileManager.default.temporaryDirectory,
|
||||
create: true
|
||||
).appendingPathComponent("vnidrop-apple-contract-\(UUID().uuidString)", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
|
||||
let sink = RecordingSink()
|
||||
let first = try VnidropCore.initializeWithExperimentalSavedDevices(
|
||||
appDataDir: directory.path,
|
||||
eventSink: sink,
|
||||
limits: defaultCoreLimits(),
|
||||
networkConfig: CoreNetworkConfig(mode: .automatic, relayUrls: [])
|
||||
)
|
||||
let endpointId = first.status().endpointId
|
||||
XCTAssertFalse(endpointId.isEmpty)
|
||||
XCTAssertFalse(
|
||||
FileManager.default.fileExists(atPath: directory.appendingPathComponent("iroh.secret").path),
|
||||
"protected identity must not fall back to plaintext"
|
||||
)
|
||||
first.shutdown()
|
||||
|
||||
let restarted = try VnidropCore.initializeWithExperimentalSavedDevices(
|
||||
appDataDir: directory.path,
|
||||
eventSink: RecordingSink(),
|
||||
limits: defaultCoreLimits(),
|
||||
networkConfig: CoreNetworkConfig(mode: .automatic, relayUrls: [])
|
||||
)
|
||||
defer { restarted.shutdown() }
|
||||
XCTAssertEqual(restarted.status().endpointId, endpointId)
|
||||
}
|
||||
|
||||
func testEventRevisionRecoveryUsesStableIdsThenListApis() throws {
|
||||
let directory = try FileManager.default.url(
|
||||
for: .itemReplacementDirectory,
|
||||
in: .userDomainMask,
|
||||
appropriateFor: FileManager.default.temporaryDirectory,
|
||||
create: true
|
||||
).appendingPathComponent("vnidrop-apple-events-\(UUID().uuidString)", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
|
||||
let sink = RecordingSink()
|
||||
let core = try VnidropCore.initializeWithExperimentalSavedDevices(
|
||||
appDataDir: directory.path,
|
||||
eventSink: sink,
|
||||
limits: defaultCoreLimits(),
|
||||
networkConfig: CoreNetworkConfig(mode: .automatic, relayUrls: [])
|
||||
)
|
||||
defer { core.shutdown() }
|
||||
|
||||
// Force at least one durable event through the public surface.
|
||||
XCTAssertThrowsError(
|
||||
try core.receive(ticket: "not-a-ticket", outputDir: directory.path, receiverName: nil)
|
||||
)
|
||||
|
||||
let listed = try core.listEvents(transferId: nil)
|
||||
XCTAssertFalse(listed.isEmpty)
|
||||
|
||||
var seenIds = Set<String>()
|
||||
var revisions = Set<UInt64>()
|
||||
for event in listed {
|
||||
XCTAssertTrue(seenIds.insert(event.id).inserted, "event ids must be stable/unique")
|
||||
XCTAssertGreaterThanOrEqual(event.revision, 1)
|
||||
XCTAssertTrue(revisions.insert(event.revision).inserted, "revisions must be distinct")
|
||||
}
|
||||
|
||||
// Simulate duplicate delivery after a listener restart, then trust list APIs.
|
||||
let duplicates = listed
|
||||
var recoveredIds = Set<String>()
|
||||
var maxRevision: UInt64 = 0
|
||||
for event in listed + duplicates {
|
||||
if recoveredIds.insert(event.id).inserted {
|
||||
maxRevision = max(maxRevision, event.revision)
|
||||
}
|
||||
}
|
||||
XCTAssertEqual(recoveredIds.count, listed.count)
|
||||
XCTAssertGreaterThanOrEqual(maxRevision, 1)
|
||||
|
||||
XCTAssertEqual(try core.listSavedDevices().count, 0)
|
||||
XCTAssertEqual(try core.listDeviceRelationships().count, 0)
|
||||
XCTAssertEqual(try core.listBlockedDevices().count, 0)
|
||||
}
|
||||
|
||||
func testGeneratedBindingsOmitRawSecretsAndGenericMutation() throws {
|
||||
let candidates = [
|
||||
Bundle(for: SavedDeviceCoreContractTests.self).bundleURL
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("Vnidrop.swift"),
|
||||
URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("VnidropCore/Sources/VnidropCore/Vnidrop.swift"),
|
||||
]
|
||||
|
||||
guard let bindingsURL = candidates.first(where: { FileManager.default.fileExists(atPath: $0.path) })
|
||||
else {
|
||||
// Source package path may be absent in a clean CI checkout before
|
||||
// build-core; linking the typed APIs below still proves the
|
||||
// regenerated surface is what the harness compiles against.
|
||||
let _: (
|
||||
(String, CoreEventSink, CoreLimits, CoreNetworkConfig) throws -> VnidropCore
|
||||
) = VnidropCore.initializeWithExperimentalSavedDevices
|
||||
XCTAssertNotNil(defaultCoreLimits().maxSavedDevices)
|
||||
return
|
||||
}
|
||||
|
||||
let source = try String(contentsOf: bindingsURL, encoding: .utf8)
|
||||
let forbidden = [
|
||||
"SecretMaterial",
|
||||
"SecretHandle",
|
||||
"SecureSecretStore",
|
||||
"executeSql",
|
||||
"executeSQL",
|
||||
"mutateState",
|
||||
"applyRawState",
|
||||
"rawSecret",
|
||||
"grantSecret",
|
||||
"pairingCapabilityBytes",
|
||||
"func setState(",
|
||||
"func mutate(",
|
||||
]
|
||||
for needle in forbidden {
|
||||
XCTAssertFalse(
|
||||
source.contains(needle),
|
||||
"generated bindings must not expose \(needle)"
|
||||
)
|
||||
}
|
||||
XCTAssertTrue(source.contains("initializeWithExperimentalSavedDevices"))
|
||||
XCTAssertTrue(source.contains("setSavedDeviceLabel"))
|
||||
XCTAssertTrue(source.contains("listSavedDevices"))
|
||||
XCTAssertTrue(source.contains("revision"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user