mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-12 05:29:57 +02:00
168 lines
5.7 KiB
Swift
168 lines
5.7 KiB
Swift
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"))
|
|
}
|
|
}
|