Buckets:
localhost1978/localhost / LOCALHOST1978 /Modules /SSK_Foundation /Tests /SSK_FoundationTests /VaultLifecycleControllerTests.swift
| #if os(macOS) | |
| import Foundation | |
| import SSK_Abstractions | |
| import XCTest | |
| @testable import SSK_Foundation | |
| final class VaultLifecycleControllerTests: XCTestCase { | |
| func testFullHostLifecycleUsesRegistryIdentityAndStopsServiceBeforeLock() async throws { | |
| let fixture = try VaultLifecycleFixture() | |
| defer { fixture.removeFiles() } | |
| let createResponse = await fixture.controller.response( | |
| for: .create( | |
| imageURL: fixture.imageURL, | |
| maximumSizeGB: 20, | |
| volumeName: "Vault" | |
| ) | |
| ) | |
| let record = try XCTUnwrap(createResponse.record) | |
| XCTAssertNil(createResponse.failure) | |
| let persistedRecords = await fixture.persistence.savedRecords() | |
| XCTAssertEqual(persistedRecords, [record]) | |
| let keyRequests = await fixture.persistence.createdKeyIDs() | |
| XCTAssertEqual(keyRequests, [record.id]) | |
| let lookupResponse = await fixture.controller.response( | |
| for: .lookup(imageURL: fixture.imageURL) | |
| ) | |
| XCTAssertEqual(lookupResponse.record, record) | |
| let mountResponse = await fixture.controller.response( | |
| for: .mount(vaultID: record.id) | |
| ) | |
| let mounted = try XCTUnwrap(mountResponse.status) | |
| XCTAssertEqual(mounted.state, .mounted) | |
| XCTAssertEqual( | |
| mounted.mountURL, | |
| fixture.mountRoot.appendingPathComponent(record.id.description) | |
| ) | |
| XCTAssertFalse(try XCTUnwrap(mounted.mountURL).path.hasPrefix("/Volumes/")) | |
| let diskMount = try XCTUnwrap(fixture.disk.lastMount()) | |
| XCTAssertEqual(diskMount.imageURL, record.imageURL) | |
| XCTAssertEqual(diskMount.expectedUUID, record.apfsVolumeUUID) | |
| XCTAssertEqual(diskMount.passphrase, "passphrase-\(record.id.description)") | |
| let leaseResponse = await fixture.controller.response( | |
| for: .acquireLease(vaultID: record.id, durationSeconds: 600) | |
| ) | |
| let lease = try XCTUnwrap(leaseResponse.lease) | |
| XCTAssertEqual(lease.vaultID, record.id) | |
| let serviceEventsAfterStart = await fixture.service.events() | |
| XCTAssertEqual(serviceEventsAfterStart, [.start(record.id, try XCTUnwrap(mounted.mountURL))]) | |
| let blockedUnmount = await fixture.controller.response( | |
| for: .unmount(vaultID: record.id) | |
| ) | |
| XCTAssertEqual(blockedUnmount.failure?.code, .vaultInUse) | |
| XCTAssertTrue(fixture.disk.unmountedSessions().isEmpty) | |
| let releaseResponse = await fixture.controller.response( | |
| for: .releaseLease(vaultID: record.id, leaseID: lease.id) | |
| ) | |
| XCTAssertNil(releaseResponse.failure) | |
| let unmountResponse = await fixture.controller.response( | |
| for: .unmount(vaultID: record.id) | |
| ) | |
| XCTAssertNil(unmountResponse.failure) | |
| XCTAssertEqual(unmountResponse.status?.state, .unmounted) | |
| XCTAssertEqual(fixture.disk.unmountedSessions().count, 1) | |
| let serviceEvents = await fixture.service.events() | |
| XCTAssertEqual( | |
| serviceEvents, | |
| [ | |
| .start(record.id, try XCTUnwrap(mounted.mountURL)), | |
| .stop(record.id), | |
| ] | |
| ) | |
| } | |
| func testExistingUnregisteredSparsebundleIsRejectedAsLegacy() async throws { | |
| let fixture = try VaultLifecycleFixture() | |
| defer { fixture.removeFiles() } | |
| try FileManager.default.createDirectory( | |
| at: fixture.imageURL, | |
| withIntermediateDirectories: false | |
| ) | |
| let response = await fixture.controller.response( | |
| for: .lookup(imageURL: fixture.imageURL) | |
| ) | |
| XCTAssertEqual(response.failure?.code, .vaultNotRegistered) | |
| XCTAssertNil(response.record) | |
| XCTAssertTrue(fixture.disk.createdImages().isEmpty) | |
| } | |
| func testTwoVaultsReceiveSeparateUUIDsAndKeys() async throws { | |
| let fixture = try VaultLifecycleFixture() | |
| defer { fixture.removeFiles() } | |
| let secondURL = fixture.root.appendingPathComponent("Second.sparsebundle") | |
| let first = await fixture.controller.response( | |
| for: .create( | |
| imageURL: fixture.imageURL, | |
| maximumSizeGB: 20, | |
| volumeName: "First" | |
| ) | |
| ) | |
| let second = await fixture.controller.response( | |
| for: .create( | |
| imageURL: secondURL, | |
| maximumSizeGB: 40, | |
| volumeName: "Second" | |
| ) | |
| ) | |
| let firstRecord = try XCTUnwrap(first.record) | |
| let secondRecord = try XCTUnwrap(second.record) | |
| XCTAssertNotEqual(firstRecord.id, secondRecord.id) | |
| XCTAssertNotEqual(firstRecord.apfsVolumeUUID, secondRecord.apfsVolumeUUID) | |
| let keyIDs = await fixture.persistence.createdKeyIDs() | |
| XCTAssertEqual(Set(keyIDs), Set([firstRecord.id, secondRecord.id])) | |
| let savedRecords = await fixture.persistence.savedRecords() | |
| XCTAssertEqual(Set(savedRecords.map(\.id)), Set([firstRecord.id, secondRecord.id])) | |
| } | |
| func testDiskUnmountFailureKeepsSessionMountedForRetry() async throws { | |
| let fixture = try VaultLifecycleFixture() | |
| defer { fixture.removeFiles() } | |
| let created = await fixture.controller.response( | |
| for: .create( | |
| imageURL: fixture.imageURL, | |
| maximumSizeGB: 20, | |
| volumeName: "Vault" | |
| ) | |
| ) | |
| let record = try XCTUnwrap(created.record) | |
| _ = await fixture.controller.response(for: .mount(vaultID: record.id)) | |
| fixture.disk.setUnmountFailure(true) | |
| let failed = await fixture.controller.response(for: .unmount(vaultID: record.id)) | |
| XCTAssertEqual(failed.failure?.code, .diskOperationFailed) | |
| let status = await fixture.controller.response(for: .status(vaultID: record.id)) | |
| XCTAssertEqual(status.status?.state, .mounted) | |
| fixture.disk.setUnmountFailure(false) | |
| let retried = await fixture.controller.response(for: .unmount(vaultID: record.id)) | |
| XCTAssertNil(retried.failure) | |
| XCTAssertEqual(retried.status?.state, .unmounted) | |
| } | |
| func testMissingRegisteredKeyFailsWithoutAttachingDisk() async throws { | |
| let fixture = try VaultLifecycleFixture() | |
| defer { fixture.removeFiles() } | |
| let created = await fixture.controller.response( | |
| for: .create( | |
| imageURL: fixture.imageURL, | |
| maximumSizeGB: 20, | |
| volumeName: "Vault" | |
| ) | |
| ) | |
| let record = try XCTUnwrap(created.record) | |
| await fixture.persistence.deletePassphrase(for: record.id) | |
| let response = await fixture.controller.response(for: .mount(vaultID: record.id)) | |
| XCTAssertEqual(response.failure?.code, .keyUnavailable) | |
| XCTAssertNil(fixture.disk.lastMount()) | |
| } | |
| func testLookupRejectsSymlinkSparsebundle() async throws { | |
| let fixture = try VaultLifecycleFixture() | |
| defer { fixture.removeFiles() } | |
| let realDirectory = fixture.root.appendingPathComponent("real", isDirectory: true) | |
| try FileManager.default.createDirectory(at: realDirectory, withIntermediateDirectories: false) | |
| try FileManager.default.createSymbolicLink( | |
| at: fixture.imageURL, | |
| withDestinationURL: realDirectory | |
| ) | |
| let response = await fixture.controller.response( | |
| for: .lookup(imageURL: fixture.imageURL) | |
| ) | |
| XCTAssertEqual(response.failure?.code, .invalidPath) | |
| } | |
| func testInvalidCreateSettingsFailBeforeKeyOrDiskWork() async throws { | |
| let fixture = try VaultLifecycleFixture() | |
| defer { fixture.removeFiles() } | |
| let response = await fixture.controller.response( | |
| for: .create( | |
| imageURL: fixture.imageURL, | |
| maximumSizeGB: 0, | |
| volumeName: "Bad/Name" | |
| ) | |
| ) | |
| XCTAssertEqual(response.failure?.code, .invalidConfiguration) | |
| let createdKeyIDs = await fixture.persistence.createdKeyIDs() | |
| XCTAssertTrue(createdKeyIDs.isEmpty) | |
| XCTAssertTrue(fixture.disk.createdImages().isEmpty) | |
| } | |
| func testBackgroundServiceStartFailureRollsBackLease() async throws { | |
| let fixture = try VaultLifecycleFixture() | |
| defer { fixture.removeFiles() } | |
| await fixture.service.setStartFailure(true) | |
| let created = await fixture.controller.response( | |
| for: .create( | |
| imageURL: fixture.imageURL, | |
| maximumSizeGB: 20, | |
| volumeName: "Vault" | |
| ) | |
| ) | |
| let record = try XCTUnwrap(created.record) | |
| _ = await fixture.controller.response(for: .mount(vaultID: record.id)) | |
| let lease = await fixture.controller.response( | |
| for: .acquireLease(vaultID: record.id, durationSeconds: 600) | |
| ) | |
| XCTAssertEqual(lease.failure?.code, .backgroundServiceFailed) | |
| let status = await fixture.controller.response(for: .status(vaultID: record.id)) | |
| XCTAssertEqual(status.status?.activeLeaseCount, 0) | |
| XCTAssertEqual(status.status?.state, .mounted) | |
| let unmount = await fixture.controller.response(for: .unmount(vaultID: record.id)) | |
| XCTAssertNil(unmount.failure) | |
| } | |
| func testBackgroundServiceStopFailureKeepsLeaseAndBlocksLock() async throws { | |
| let fixture = try VaultLifecycleFixture() | |
| defer { fixture.removeFiles() } | |
| let created = await fixture.controller.response( | |
| for: .create( | |
| imageURL: fixture.imageURL, | |
| maximumSizeGB: 20, | |
| volumeName: "Vault" | |
| ) | |
| ) | |
| let record = try XCTUnwrap(created.record) | |
| _ = await fixture.controller.response(for: .mount(vaultID: record.id)) | |
| let leaseResponse = await fixture.controller.response( | |
| for: .acquireLease(vaultID: record.id, durationSeconds: 600) | |
| ) | |
| let lease = try XCTUnwrap(leaseResponse.lease) | |
| await fixture.service.setStopFailure(true) | |
| let release = await fixture.controller.response( | |
| for: .releaseLease(vaultID: record.id, leaseID: lease.id) | |
| ) | |
| XCTAssertEqual(release.failure?.code, .backgroundServiceFailed) | |
| let status = await fixture.controller.response(for: .status(vaultID: record.id)) | |
| XCTAssertEqual(status.status?.activeLeaseCount, 1) | |
| let unmount = await fixture.controller.response(for: .unmount(vaultID: record.id)) | |
| XCTAssertEqual(unmount.failure?.code, .vaultInUse) | |
| XCTAssertTrue(fixture.disk.unmountedSessions().isEmpty) | |
| } | |
| func testSecondVaultCannotTakeBackgroundServiceWhileFirstIsActive() async throws { | |
| let fixture = try VaultLifecycleFixture() | |
| defer { fixture.removeFiles() } | |
| let secondURL = fixture.root.appendingPathComponent("Second.sparsebundle") | |
| let firstResponse = await fixture.controller.response( | |
| for: .create( | |
| imageURL: fixture.imageURL, | |
| maximumSizeGB: 20, | |
| volumeName: "First" | |
| ) | |
| ) | |
| let first = try XCTUnwrap(firstResponse.record) | |
| let secondResponse = await fixture.controller.response( | |
| for: .create( | |
| imageURL: secondURL, | |
| maximumSizeGB: 20, | |
| volumeName: "Second" | |
| ) | |
| ) | |
| let second = try XCTUnwrap(secondResponse.record) | |
| _ = await fixture.controller.response(for: .mount(vaultID: first.id)) | |
| _ = await fixture.controller.response(for: .mount(vaultID: second.id)) | |
| _ = await fixture.controller.response( | |
| for: .acquireLease(vaultID: first.id, durationSeconds: 600) | |
| ) | |
| let secondLease = await fixture.controller.response( | |
| for: .acquireLease(vaultID: second.id, durationSeconds: 600) | |
| ) | |
| XCTAssertEqual(secondLease.failure?.code, .vaultInUse) | |
| let secondStatus = await fixture.controller.response(for: .status(vaultID: second.id)) | |
| XCTAssertEqual(secondStatus.status?.activeLeaseCount, 0) | |
| } | |
| func testShutdownStopsBackgroundServiceBeforeDiskUnmount() async throws { | |
| let root = FileManager.default.temporaryDirectory | |
| .appendingPathComponent("ssk-shutdown-order-\(UUID().uuidString)") | |
| try FileManager.default.createDirectory( | |
| at: root, | |
| withIntermediateDirectories: false, | |
| attributes: [.posixPermissions: 0o700] | |
| ) | |
| defer { try? FileManager.default.removeItem(at: root) } | |
| let persistence = TestVaultPersistence() | |
| let events = ShutdownOrderEvents() | |
| let disk = TestVaultDiskOperator(events: events) | |
| let credentials = VaultCredentialStore( | |
| getOrCreatePassphrase: { id in | |
| await persistence.createPassphrase(for: id) | |
| }, | |
| existingPassphrase: { id in | |
| await persistence.passphrase(for: id) | |
| }, | |
| deleteKey: { id in | |
| await persistence.deletePassphrase(for: id) | |
| }, | |
| loadRecords: { await persistence.savedRecords() }, | |
| saveRecords: { records in await persistence.save(records: records) } | |
| ) | |
| let controller = VaultLifecycleController( | |
| credentials: credentials, | |
| diskOperator: disk, | |
| mountRootURL: root.appendingPathComponent("mounts"), | |
| backgroundHooks: VaultBackgroundServiceHooks( | |
| start: { _, _ in }, | |
| stop: { _ in events.append(.serviceStop) } | |
| ) | |
| ) | |
| let imageURL = root.appendingPathComponent("Vault.sparsebundle") | |
| let createResponse = await controller.response( | |
| for: .create( | |
| imageURL: imageURL, | |
| maximumSizeGB: 20, | |
| volumeName: "Vault" | |
| ) | |
| ) | |
| let record = try XCTUnwrap(createResponse.record) | |
| _ = await controller.response(for: .mount(vaultID: record.id)) | |
| _ = await controller.response( | |
| for: .acquireLease(vaultID: record.id, durationSeconds: 600) | |
| ) | |
| try await controller.shutdown() | |
| XCTAssertEqual(events.values(), [.serviceStop, .diskUnmount]) | |
| } | |
| } | |
| private struct VaultLifecycleFixture { | |
| let root: URL | |
| let imageURL: URL | |
| let mountRoot: URL | |
| let persistence: TestVaultPersistence | |
| let disk: TestVaultDiskOperator | |
| let service: TestVaultBackgroundService | |
| let controller: VaultLifecycleController | |
| init() throws { | |
| root = FileManager.default.temporaryDirectory | |
| .appendingPathComponent("ssk-vault-tests-\(UUID().uuidString)", isDirectory: true) | |
| try FileManager.default.createDirectory( | |
| at: root, | |
| withIntermediateDirectories: false, | |
| attributes: [.posixPermissions: 0o700] | |
| ) | |
| imageURL = root.appendingPathComponent("Vault.sparsebundle", isDirectory: true) | |
| mountRoot = root.appendingPathComponent("mounts", isDirectory: true) | |
| persistence = TestVaultPersistence() | |
| disk = TestVaultDiskOperator() | |
| service = TestVaultBackgroundService() | |
| let persistence = self.persistence | |
| let credentials = VaultCredentialStore( | |
| getOrCreatePassphrase: { id in | |
| await persistence.createPassphrase(for: id) | |
| }, | |
| existingPassphrase: { id in | |
| await persistence.passphrase(for: id) | |
| }, | |
| deleteKey: { id in | |
| await persistence.deletePassphrase(for: id) | |
| }, | |
| loadRecords: { | |
| await persistence.savedRecords() | |
| }, | |
| saveRecords: { records in | |
| await persistence.save(records: records) | |
| } | |
| ) | |
| let service = self.service | |
| let hooks = VaultBackgroundServiceHooks( | |
| start: { id, mountURL in | |
| try await service.start(id: id, mountURL: mountURL) | |
| }, | |
| stop: { id in | |
| try await service.stop(id: id) | |
| } | |
| ) | |
| controller = VaultLifecycleController( | |
| credentials: credentials, | |
| diskOperator: disk, | |
| mountRootURL: mountRoot, | |
| backgroundHooks: hooks | |
| ) | |
| } | |
| func removeFiles() { | |
| try? FileManager.default.removeItem(at: root) | |
| } | |
| } | |
| private actor TestVaultPersistence { | |
| private var records: [VaultRecord] = [] | |
| private var passphrases: [VaultID: String] = [:] | |
| private var keyCreationOrder: [VaultID] = [] | |
| func createPassphrase(for id: VaultID) -> String { | |
| if let existing = passphrases[id] { return existing } | |
| let value = "passphrase-\(id.description)" | |
| passphrases[id] = value | |
| keyCreationOrder.append(id) | |
| return value | |
| } | |
| func passphrase(for id: VaultID) -> String? { | |
| passphrases[id] | |
| } | |
| func deletePassphrase(for id: VaultID) { | |
| passphrases.removeValue(forKey: id) | |
| } | |
| func save(records: [VaultRecord]) { | |
| self.records = records | |
| } | |
| func savedRecords() -> [VaultRecord] { | |
| records | |
| } | |
| func createdKeyIDs() -> [VaultID] { | |
| keyCreationOrder | |
| } | |
| } | |
| private final class TestVaultDiskOperator: VaultDiskOperating, @unchecked Sendable { | |
| struct MountCall: Equatable { | |
| let imageURL: URL | |
| let passphrase: String | |
| let mountPoint: URL | |
| let expectedUUID: UUID | |
| } | |
| private let lock = NSLock() | |
| private var creations: [URL] = [] | |
| private var mountCall: MountCall? | |
| private var unmounts: [VaultDiskSession] = [] | |
| private var shouldFailUnmount = false | |
| private let events: ShutdownOrderEvents? | |
| init(events: ShutdownOrderEvents? = nil) { | |
| self.events = events | |
| } | |
| func createVault( | |
| at imageURL: URL, | |
| passphrase: String, | |
| configuration: SparsebundleConfig | |
| ) throws -> VaultDiskCreation { | |
| try FileManager.default.createDirectory( | |
| at: imageURL, | |
| withIntermediateDirectories: false, | |
| attributes: [.posixPermissions: 0o700] | |
| ) | |
| lock.withLock { creations.append(imageURL) } | |
| return VaultDiskCreation(apfsVolumeUUID: UUID()) | |
| } | |
| func mountVault( | |
| at imageURL: URL, | |
| passphrase: String, | |
| mountPoint: URL, | |
| expectedVolumeUUID: UUID | |
| ) throws -> VaultDiskSession { | |
| lock.withLock { | |
| mountCall = MountCall( | |
| imageURL: imageURL, | |
| passphrase: passphrase, | |
| mountPoint: mountPoint, | |
| expectedUUID: expectedVolumeUUID | |
| ) | |
| } | |
| return VaultDiskSession( | |
| mountURL: mountPoint, | |
| apfsVolume: URL(fileURLWithPath: "/dev/disk99s1"), | |
| wholeDisk: URL(fileURLWithPath: "/dev/disk99"), | |
| apfsVolumeUUID: expectedVolumeUUID | |
| ) | |
| } | |
| func unmountVault(_ session: VaultDiskSession) throws { | |
| let fail = lock.withLock { shouldFailUnmount } | |
| if fail { | |
| throw SparsebundleError.unmountFailed(1) | |
| } | |
| lock.withLock { unmounts.append(session) } | |
| events?.append(.diskUnmount) | |
| } | |
| func createdImages() -> [URL] { | |
| lock.withLock { creations } | |
| } | |
| func lastMount() -> MountCall? { | |
| lock.withLock { mountCall } | |
| } | |
| func unmountedSessions() -> [VaultDiskSession] { | |
| lock.withLock { unmounts } | |
| } | |
| func setUnmountFailure(_ value: Bool) { | |
| lock.withLock { shouldFailUnmount = value } | |
| } | |
| } | |
| private final class ShutdownOrderEvents: @unchecked Sendable { | |
| enum Event: Equatable { | |
| case serviceStop | |
| case diskUnmount | |
| } | |
| private let lock = NSLock() | |
| private var storage: [Event] = [] | |
| func append(_ event: Event) { | |
| lock.withLock { storage.append(event) } | |
| } | |
| func values() -> [Event] { | |
| lock.withLock { storage } | |
| } | |
| } | |
| private actor TestVaultBackgroundService { | |
| private enum TestFailure: Error { | |
| case start | |
| case stop | |
| } | |
| enum Event: Equatable { | |
| case start(VaultID, URL) | |
| case stop(VaultID) | |
| } | |
| private var recordedEvents: [Event] = [] | |
| private var shouldFailStart = false | |
| private var shouldFailStop = false | |
| func start(id: VaultID, mountURL: URL) throws { | |
| if shouldFailStart { throw TestFailure.start } | |
| recordedEvents.append(.start(id, mountURL)) | |
| } | |
| func stop(id: VaultID) throws { | |
| if shouldFailStop { throw TestFailure.stop } | |
| recordedEvents.append(.stop(id)) | |
| } | |
| func setStartFailure(_ value: Bool) { | |
| shouldFailStart = value | |
| } | |
| func setStopFailure(_ value: Bool) { | |
| shouldFailStop = value | |
| } | |
| func events() -> [Event] { | |
| recordedEvents | |
| } | |
| } | |
| #endif | |
Xet Storage Details
- Size:
- 21 kB
- Xet hash:
- 687640c1f6b4f0b1f593e52af25a2efcd8a5a22f0c7c43574b66b61c3e1cb5cd
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.