commit d1c36a8617f5893351408aee3f7fa840761a4615
parent 16e1fbbe669c662ca82ac95de98df2fc6728159f
Author: triesap <tyson@radroots.org>
Date: Sat, 29 Aug 2026 05:18:02 +0000
ios: qualify local-social development candidate
- add governed Swift formatting, linting, and Linux x86_64 Rust lanes
- make simulator text sizing deterministic and restore ambient state
- harden visible save, media, and persisted-outbox UI evidence
- bind package, source-lock, API, artifact, and runtime qualification
Diffstat:
43 files changed, 1544 insertions(+), 1311 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -49,6 +49,8 @@ Thumbs.db
# Testing
test*.json
+__pycache__/
+*.py[cod]
# Editors
.vscode/
diff --git a/.swiftformat b/.swiftformat
@@ -0,0 +1,4 @@
+--exclude .build,RadrootsFFI/.build,Radroots/Generated,Radroots.xcodeproj,.swiftpm
+--indent 2
+--swift-version 6.0
+--disable extensionAccessControl,indent,redundantFileprivate
diff --git a/.swiftlint.yml b/.swiftlint.yml
@@ -0,0 +1,24 @@
+excluded:
+ - .build
+ - .swiftpm
+ - Radroots.xcodeproj
+ - Radroots/Generated
+ - RadrootsFFI/.build
+
+disabled_rules:
+ # SwiftFormat is authoritative for overlapping structural style rules.
+ - closure_parameter_position
+ - opening_brace
+ - trailing_comma
+ # Existing package architecture is governed by tests and API checks; these
+ # metric and naming policies are not retrofit as Step 070 admission gates.
+ - cyclomatic_complexity
+ - file_length
+ - function_body_length
+ - function_parameter_count
+ - identifier_name
+ - large_tuple
+ - line_length
+ - multiple_closures_with_trailing_closure
+ - type_body_length
+ - type_name
diff --git a/AGENTS.md b/AGENTS.md
@@ -76,6 +76,12 @@ This file applies to the complete standalone iOS app repository. A closer
`cargo extbuild run -- ...`.
- `make package-contract-check` is the narrow standalone source-lock,
package-lock, privacy, version, and forbidden-root guard.
+- `make swift-quality` applies the checked-in SwiftFormat and SwiftLint policy
+ to repository-owned package, app, unit-test, API-test, and UI-test sources;
+ generated bindings and dependency/build output are excluded.
+- `make linux-shared-rust` runs the locked source-lock workspace in the pinned
+ Linux x86_64 Rust runner while keeping Cargo caches and output under the
+ extbuild project root.
- `make bootstrap` performs networked source/artifact bootstrap. `make verify`
is the complete package, Xcode build/test, UI, and API-snapshot lane. Use an
explicitly installed simulator name when the default is unavailable.
diff --git a/Makefile b/Makefile
@@ -8,6 +8,8 @@ SIMULATOR_DESTINATION := platform=iOS Simulator,name=$(SIMULATOR_NAME)
.NOTPARALLEL:
.PHONY: all doctor bootstrap ffi-bootstrap artifact-check package-contract-check \
+ swift-quality \
+ linux-shared-rust \
package-resolve package-build package-test project xcodegen xcode-resolve \
xcode-build-debug xcode-build-release unit-test ui-test api-snapshot-write \
api-snapshot-check release-evidence-write release-preflight verify clean distclean
@@ -26,6 +28,12 @@ artifact-check: doctor
package-contract-check: doctor
cargo extbuild run -- scripts/verify-package-contract.sh
+swift-quality: doctor
+ cargo extbuild run -- scripts/swift-quality.sh
+
+linux-shared-rust: doctor
+ cargo extbuild run -- scripts/linux-shared-rust.sh
+
package-resolve: artifact-check package-contract-check
cargo extbuild run -- scripts/swift-package.sh resolve
@@ -67,7 +75,7 @@ release-evidence-write: doctor
release-preflight: artifact-check package-contract-check
cargo extbuild run -- scripts/release-preflight.sh
-verify: artifact-check package-contract-check package-build package-test \
+verify: swift-quality linux-shared-rust artifact-check package-contract-check package-build package-test \
xcode-build-debug xcode-build-release unit-test ui-test api-snapshot-check
clean: doctor
diff --git a/Package.swift b/Package.swift
@@ -3,85 +3,85 @@
import PackageDescription
let package = Package(
- name: "radroots_ios_app",
- platforms: [
- .iOS(.v18),
- ],
- products: [
- .library(name: "RadrootsApp", targets: ["RadrootsApp"]),
- ],
- dependencies: [
- .package(
- url: "https://github.com/radrootslabs/apple_kit.git",
- revision: "e61386529422a6413a852f0233a196a88f4ded51"
- ),
- ],
- targets: [
- .binaryTarget(
- name: "RadrootsFFI",
- path: "Radroots/Frameworks/RadrootsFFI.xcframework"
- ),
- .target(
- name: "RadrootsKitBindings",
- dependencies: ["RadrootsFFI"],
- path: "Radroots/Generated",
- swiftSettings: [
- .swiftLanguageMode(.v5),
- ]
- ),
- .target(
- name: "RadrootsApp",
- dependencies: [
- "RadrootsKitBindings",
- .product(name: "RadrootsKit", package: "apple_kit"),
- ],
- path: "Radroots",
- exclude: [
- "App/App.swift",
- "Config",
- "Frameworks",
- "Generated",
- "Info.plist",
- "radroots.xcconfig",
- ],
- sources: [
- "App/AppEntry.swift",
- "App/RadrootsAppDelegate.swift",
- "App/RadrootsAppModel.swift",
- "App/RadrootsProvider.swift",
- "App/RadrootsRemoteQualification.swift",
- "App/RadrootsRootShell.swift",
- "Runtime/RadrootsAddMediaCoordinator.swift",
- "Runtime/RadrootsGeneratedRuntimeBackend.swift",
- "Runtime/RadrootsLifecycleCoordinator.swift",
- "Runtime/RadrootsRuntimeClient.swift",
- "Runtime/RadrootsRuntimeModels.swift",
- "State/RadrootsAddStore.swift",
- "State/RadrootsConfigurationStore.swift",
- "State/RadrootsIdentityStore.swift",
- "State/RadrootsMediaStore.swift",
- "State/RadrootsSessionStore.swift",
- "State/RadrootsSupportingStores.swift",
- "State/RadrootsTodayStore.swift",
- "Views/RadrootsAddView.swift",
- "Views/RadrootsSupportingViews.swift",
- "Views/RadrootsTodayView.swift",
- "Views/RuntimeStatusView.swift",
- ],
- resources: [
- .process("Resources/PrivacyInfo.xcprivacy"),
- ]
- ),
- .testTarget(
- name: "RadrootsAppTests",
- dependencies: ["RadrootsApp"],
- path: "RadrootsTests"
- ),
- .testTarget(
- name: "RadrootsAppPublicAPITests",
- dependencies: ["RadrootsApp"],
- path: "RadrootsPublicAPITests"
- ),
- ],
- swiftLanguageModes: [.v6]
+ name: "radroots_ios_app",
+ platforms: [
+ .iOS(.v18),
+ ],
+ products: [
+ .library(name: "RadrootsApp", targets: ["RadrootsApp"]),
+ ],
+ dependencies: [
+ .package(
+ url: "https://github.com/radrootslabs/apple_kit.git",
+ revision: "e61386529422a6413a852f0233a196a88f4ded51"
+ ),
+ ],
+ targets: [
+ .binaryTarget(
+ name: "RadrootsFFI",
+ path: "Radroots/Frameworks/RadrootsFFI.xcframework"
+ ),
+ .target(
+ name: "RadrootsKitBindings",
+ dependencies: ["RadrootsFFI"],
+ path: "Radroots/Generated",
+ swiftSettings: [
+ .swiftLanguageMode(.v5),
+ ]
+ ),
+ .target(
+ name: "RadrootsApp",
+ dependencies: [
+ "RadrootsKitBindings",
+ .product(name: "RadrootsKit", package: "apple_kit"),
+ ],
+ path: "Radroots",
+ exclude: [
+ "App/App.swift",
+ "Config",
+ "Frameworks",
+ "Generated",
+ "Info.plist",
+ "radroots.xcconfig",
+ ],
+ sources: [
+ "App/AppEntry.swift",
+ "App/RadrootsAppDelegate.swift",
+ "App/RadrootsAppModel.swift",
+ "App/RadrootsProvider.swift",
+ "App/RadrootsRemoteQualification.swift",
+ "App/RadrootsRootShell.swift",
+ "Runtime/RadrootsAddMediaCoordinator.swift",
+ "Runtime/RadrootsGeneratedRuntimeBackend.swift",
+ "Runtime/RadrootsLifecycleCoordinator.swift",
+ "Runtime/RadrootsRuntimeClient.swift",
+ "Runtime/RadrootsRuntimeModels.swift",
+ "State/RadrootsAddStore.swift",
+ "State/RadrootsConfigurationStore.swift",
+ "State/RadrootsIdentityStore.swift",
+ "State/RadrootsMediaStore.swift",
+ "State/RadrootsSessionStore.swift",
+ "State/RadrootsSupportingStores.swift",
+ "State/RadrootsTodayStore.swift",
+ "Views/RadrootsAddView.swift",
+ "Views/RadrootsSupportingViews.swift",
+ "Views/RadrootsTodayView.swift",
+ "Views/RuntimeStatusView.swift",
+ ],
+ resources: [
+ .process("Resources/PrivacyInfo.xcprivacy"),
+ ]
+ ),
+ .testTarget(
+ name: "RadrootsAppTests",
+ dependencies: ["RadrootsApp"],
+ path: "RadrootsTests"
+ ),
+ .testTarget(
+ name: "RadrootsAppPublicAPITests",
+ dependencies: ["RadrootsApp"],
+ path: "RadrootsPublicAPITests"
+ ),
+ ],
+ swiftLanguageModes: [.v6]
)
diff --git a/README.md b/README.md
@@ -50,6 +50,11 @@ resolved revisions without automatic dependency updates:
make verify
```
+The complete lane includes the repository-owned Swift formatting and lint
+policy and the pinned Linux x86_64 shared-Rust runner. Run those focused checks
+independently with `make swift-quality` and `make linux-shared-rust`; both keep
+their build output under extbuild.
+
Use `SIMULATOR_NAME="Device Name" make verify` when the default simulator is
not installed. `make clean` removes only rebuildable external build output and
the ignored XCFramework; it preserves tracked generated bindings, locks,
diff --git a/Radroots/App/AppEntry.swift b/Radroots/App/AppEntry.swift
@@ -19,7 +19,7 @@ struct AppEntry: View {
var body: some View {
Group {
- if case .running(let snapshot) = appModel.phase {
+ if case let .running(snapshot) = appModel.phase {
RadrootsRootShell(
snapshot: snapshot,
stores: appModel.productStores
diff --git a/Radroots/App/RadrootsAppDelegate.swift b/Radroots/App/RadrootsAppDelegate.swift
@@ -6,8 +6,8 @@ public final class RadrootsAppDelegate: NSObject, UIApplicationDelegate {
}
public func application(
- _ application: UIApplication,
- didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
#if DEBUG
if (try? RadrootsRemoteQualificationEnvironment.current()) != nil {
@@ -18,15 +18,15 @@ public final class RadrootsAppDelegate: NSObject, UIApplicationDelegate {
}
public func application(
- _: UIApplication,
- handleEventsForBackgroundURLSession identifier: String,
- completionHandler: @escaping () -> Void
+ _: UIApplication,
+ handleEventsForBackgroundURLSession identifier: String,
+ completionHandler: @escaping () -> Void
) {
let completion = RadrootsCompletionOnce(completionHandler)
Task {
await RadrootsBackgroundEventRouter.shared.handle(
- identifier: identifier,
- completion: completion
+ identifier: identifier,
+ completion: completion
)
}
}
diff --git a/Radroots/App/RadrootsAppModel.swift b/Radroots/App/RadrootsAppModel.swift
@@ -271,7 +271,7 @@ final class RadrootsAppModel: ObservableObject {
await finishSessionOperation()
return
}
- if case .running(let snapshot) = result {
+ if case let .running(snapshot) = result {
productStores?.configure(snapshot: snapshot)
}
phase = result
diff --git a/Radroots/App/RadrootsProvider.swift b/Radroots/App/RadrootsProvider.swift
@@ -8,8 +8,8 @@ struct RadrootsProvider<Content: View>: View {
private let content: () -> Content
init(
- appModel: @autoclosure @escaping () -> RadrootsAppModel = RadrootsAppModel(),
- @ViewBuilder content: @escaping () -> Content
+ appModel: @autoclosure @escaping () -> RadrootsAppModel = RadrootsAppModel(),
+ @ViewBuilder content: @escaping () -> Content
) {
_appModel = StateObject(wrappedValue: appModel())
self.content = content
diff --git a/Radroots/App/RadrootsRemoteQualification.swift b/Radroots/App/RadrootsRemoteQualification.swift
@@ -110,7 +110,7 @@ struct RadrootsRemoteQualificationEnvironment: Sendable, Equatable {
private static func requiredRunID(_ raw: String?) throws -> String {
guard let raw,
- (8...64).contains(raw.utf8.count),
+ (8 ... 64).contains(raw.utf8.count),
raw == raw.lowercased(),
raw.unicodeScalars.allSatisfy({
CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyz0123456789-")
diff --git a/Radroots/Runtime/RadrootsAddMediaCoordinator.swift b/Radroots/Runtime/RadrootsAddMediaCoordinator.swift
@@ -353,7 +353,7 @@ actor RadrootsAddMediaCoordinator: RadrootsAddMediaHandling {
return snapshot
}
let candidates = parsed.filter { snapshot in
- return snapshot.state != .completed
+ snapshot.state != .completed
|| Self.persistedRequestMatches(snapshot.request, request: request)
}
try Task.checkCancellation()
@@ -544,7 +544,7 @@ actor RadrootsAddMediaCoordinator: RadrootsAddMediaHandling {
)
}
try RadrootsAppleFileAccess(roots: roots).write(
- .inline(try RadrootsRemoteQualificationEnvironment.mediaFixtureData()),
+ .inline(RadrootsRemoteQualificationEnvironment.mediaFixtureData()),
to: file
)
let url = try roots.resolvedURL(for: file)
@@ -554,7 +554,7 @@ actor RadrootsAddMediaCoordinator: RadrootsAddMediaHandling {
guard values.isRegularFile == true,
values.isSymbolicLink != true,
let size = values.fileSize,
- (1...40 * 1024 * 1024).contains(size)
+ (1 ... 40 * 1024 * 1024).contains(size)
else {
throw RadrootsCaptureIntakeError.unavailable(
"remote qualification image is unavailable"
diff --git a/Radroots/Runtime/RadrootsGeneratedRuntimeBackend.swift b/Radroots/Runtime/RadrootsGeneratedRuntimeBackend.swift
@@ -638,7 +638,7 @@ private final class RadrootsGeneratedRuntimeBackend: RadrootsRuntimeBackend, @un
}
private static func failure(from error: Error) -> RadrootsRuntimeFailure {
- if case RadrootsAppError.Failure(let report) = error {
+ if case let RadrootsAppError.Failure(report) = error {
return RadrootsRuntimeFailure(
schemaVersion: report.schemaVersion,
code: report.code,
@@ -781,7 +781,7 @@ extension RadrootsRuntimeSigningOutcome {
}
fileprivate var signatureHex: String? {
- if case .signed(let signatureHex) = self {
+ if case let .signed(signatureHex) = self {
return signatureHex
}
return nil
diff --git a/Radroots/Runtime/RadrootsLifecycleCoordinator.swift b/Radroots/Runtime/RadrootsLifecycleCoordinator.swift
@@ -163,7 +163,8 @@ actor RadrootsDiagnosticsBuffer: RadrootsTelemetry {
fields: Dictionary(
uniqueKeysWithValues: event.fields.map { field in
(field.key, field.value.renderedValue)
- }),
+ }
+ ),
occurredAtUnixMilliseconds: Int64(event.occurredAt.timeIntervalSince1970 * 1000)
)
}
diff --git a/Radroots/Runtime/RadrootsRuntimeClient.swift b/Radroots/Runtime/RadrootsRuntimeClient.swift
@@ -547,7 +547,7 @@ actor RadrootsRuntimeClient {
}
},
onAbandonedResult: { result in
- guard case .success(let started) = result else { return }
+ guard case let .success(started) = result else { return }
let cleanup = RadrootsRuntimeBoundedTask<RadrootsRuntimeShutdownReceipt>(
deadlineNanoseconds: cleanupDeadline,
operation: {
@@ -923,7 +923,7 @@ actor RadrootsRuntimeClient {
}
},
onAbandonedResult: { result in
- guard case .success(let token) = result else { return }
+ guard case let .success(token) = result else { return }
let cancellation = RadrootsRuntimeBoundedTask<Void>(
deadlineNanoseconds: subscriptionDeadline,
operation: {
@@ -946,18 +946,18 @@ actor RadrootsRuntimeClient {
var subscription = subscriptions[id]
else {
subscriptions.removeValue(forKey: id)?.continuation.finish()
- if case .completed(.success(let token)) = outcome {
+ if case let .completed(.success(token)) = outcome {
cancelTokenDetached(token)
}
throw RadrootsRuntimeClientError.superseded
}
switch outcome {
- case .completed(.success(let token)):
+ case let .completed(.success(token)):
subscription.token = token
subscriptions[id] = subscription
return pair.stream
- case .completed(.failure(let failure)):
+ case let .completed(.failure(failure)):
subscriptions.removeValue(forKey: id)?.continuation.finish()
throw RadrootsRuntimeClientError.subscription(failure)
case .timedOut:
@@ -1029,7 +1029,7 @@ actor RadrootsRuntimeClient {
if startupOperation?.identity == operation.identity {
startupOperation = nil
- } else if case .completed(.success(let started)) = outcome,
+ } else if case let .completed(.success(started)) = outcome,
configuration == operation.configuration,
case .running = lifecycleState
{
@@ -1039,12 +1039,12 @@ actor RadrootsRuntimeClient {
}
switch outcome {
- case .completed(.success(let started)):
+ case let .completed(.success(started)):
backend = started.backend
configuration = operation.configuration
lifecycleState = .running(generation: operation.identity.generation)
return started.snapshot
- case .completed(.failure(let failure)):
+ case let .completed(.failure(failure)):
lifecycleState = .failed(generation: operation.identity.generation, failure: failure)
throw RadrootsRuntimeClientError.startup(failure)
case .timedOut:
@@ -1140,11 +1140,11 @@ actor RadrootsRuntimeClient {
}
switch outcome {
- case .completed(.success(let receipt)):
+ case let .completed(.success(receipt)):
quarantinedBackend = nil
lifecycleState = .stopped
return receipt
- case .completed(.failure(let failure)):
+ case let .completed(.failure(failure)):
quarantinedBackend = operation.backend
lifecycleState = .failed(generation: operation.identity.generation, failure: failure)
throw RadrootsRuntimeClientError.shutdown(failure)
@@ -1197,9 +1197,9 @@ actor RadrootsRuntimeClient {
throw RadrootsRuntimeClientError.superseded
}
switch outcome {
- case .completed(.success(let value)):
+ case let .completed(.success(value)):
return value
- case .completed(.failure(let failure)):
+ case let .completed(.failure(failure)):
throw failure
case .timedOut:
throw Self.deadlineFailure(identity: identity)
@@ -1341,25 +1341,25 @@ actor RadrootsRuntimeClient {
safeMessage: "The Radroots runtime operation was cancelled."
)
}
- if case RadrootsRuntimeClientError.startup(let failure) = error {
+ if case let RadrootsRuntimeClientError.startup(failure) = error {
return failure
}
- if case RadrootsRuntimeClientError.subscription(let failure) = error {
+ if case let RadrootsRuntimeClientError.subscription(failure) = error {
return failure
}
- if case RadrootsRuntimeClientError.status(let failure) = error {
+ if case let RadrootsRuntimeClientError.status(failure) = error {
return failure
}
- if case RadrootsRuntimeClientError.today(let failure) = error {
+ if case let RadrootsRuntimeClientError.today(failure) = error {
return failure
}
- if case RadrootsRuntimeClientError.add(let failure) = error {
+ if case let RadrootsRuntimeClientError.add(failure) = error {
return failure
}
- if case RadrootsRuntimeClientError.support(let failure) = error {
+ if case let RadrootsRuntimeClientError.support(failure) = error {
return failure
}
- if case RadrootsRuntimeClientError.shutdown(let failure) = error {
+ if case let RadrootsRuntimeClientError.shutdown(failure) = error {
return failure
}
return .local(
diff --git a/Radroots/Runtime/RadrootsRuntimeModels.swift b/Radroots/Runtime/RadrootsRuntimeModels.swift
@@ -467,13 +467,13 @@ extension RadrootsRuntimeClientError: LocalizedError {
"The Radroots runtime is not running."
case .superseded:
"A newer runtime lifecycle request replaced this request."
- case .startup(let failure),
- .subscription(let failure),
- .status(let failure),
- .today(let failure),
- .add(let failure),
- .support(let failure),
- .shutdown(let failure):
+ case let .startup(failure),
+ let .subscription(failure),
+ let .status(failure),
+ let .today(failure),
+ let .add(failure),
+ let .support(failure),
+ let .shutdown(failure):
failure.safeMessage
}
}
@@ -717,11 +717,11 @@ struct RadrootsTodayCard: Sendable, Equatable, Hashable, Identifiable {
guard sourceAddress == nil else { return nil }
return 1
case .event:
- guard let kind = sourceAddressKind, kind == 31_922 || kind == 31_923 else { return nil }
+ guard let kind = sourceAddressKind, kind == 31922 || kind == 31923 else { return nil }
return kind
case .foodAvailability:
- guard sourceAddressKind == 30_402 else { return nil }
- return 30_402
+ guard sourceAddressKind == 30402 else { return nil }
+ return 30402
}
}
@@ -887,7 +887,7 @@ enum RadrootsProductSurfaceContract {
guard schemas.map(\.commandType) == expectedCommands,
schemas.allSatisfy({ $0.schemaVersion == 1 && $0.label == $0.commandType.label }),
schemas.allSatisfy({ schema in
- schema.fields.allSatisfy({ $0.schemaVersion == 1 })
+ schema.fields.allSatisfy { $0.schemaVersion == 1 }
&& Set(schema.fields.map(\.id)).count == schema.fields.count
}),
schemas.allSatisfy({ $0.fields.map(\.id) == expectedFieldIDs(for: $0.commandType) }),
diff --git a/Radroots/State/RadrootsAddStore.swift b/Radroots/State/RadrootsAddStore.swift
@@ -50,7 +50,7 @@ final class RadrootsAddStore: ObservableObject {
UInt64(Date().timeIntervalSince1970)
},
nowUnixMilliseconds: @escaping @Sendable () -> UInt64 = {
- UInt64(Date().timeIntervalSince1970 * 1_000)
+ UInt64(Date().timeIntervalSince1970 * 1000)
},
observationDelay: @escaping @Sendable (UInt32) async throws -> Void =
RadrootsRuntimeObservationBackoff.sleep
@@ -514,7 +514,7 @@ final class RadrootsAddStore: ObservableObject {
)
try Task.checkCancellation()
revisionOperationID = revision.operationID
- self.revisionTarget = nil
+ revisionTarget = nil
accept(revision)
return revision.replacement
}
@@ -718,7 +718,7 @@ final class RadrootsAddStore: ObservableObject {
lastFailureCode = nil
let task = Task { @MainActor [weak self] in
guard let self else { return }
- await self.execute(operation, generation: requestedGeneration)
+ await execute(operation, generation: requestedGeneration)
}
operationTask = task
await task.value
@@ -771,8 +771,8 @@ final class RadrootsAddStore: ObservableObject {
}
if type == .createEvent {
let now = nowUnixSeconds()
- let start = now.addingReportingOverflow(3_600).overflow ? now : now + 3_600
- let end = start.addingReportingOverflow(3_600).overflow ? start : start + 3_600
+ let start = now.addingReportingOverflow(3600).overflow ? now : now + 3600
+ let end = start.addingReportingOverflow(3600).overflow ? start : start + 3600
form.eventStartUnixSeconds = start
form.eventEndUnixSeconds = end
form.eventStartDate = eventDateFormatter.string(
@@ -810,10 +810,9 @@ final class RadrootsAddStore: ObservableObject {
}
private static func failure(for error: Error) -> RadrootsRuntimeFailure? {
- if case RadrootsRuntimeClientError.add(let failure) = error {
+ if case let RadrootsRuntimeClientError.add(failure) = error {
return failure
}
return error as? RadrootsRuntimeFailure
}
-
}
diff --git a/Radroots/State/RadrootsConfigurationStore.swift b/Radroots/State/RadrootsConfigurationStore.swift
@@ -1,5 +1,5 @@
-import Darwin
import CryptoKit
+import Darwin
import Foundation
import RadrootsKit
import Security
@@ -113,20 +113,20 @@ actor RadrootsConfigurationStore {
}
private static let configurationFile = RadrootsFileReference(
- scope: .data,
- relativePath: "settings/radroots_configuration_v3.json"
+ scope: .data,
+ relativePath: "settings/radroots_configuration_v3.json"
)
private static let v2ConfigurationFile = RadrootsFileReference(
- scope: .data,
- relativePath: "settings/radroots_configuration_v2.json"
+ scope: .data,
+ relativePath: "settings/radroots_configuration_v2.json"
)
private static let legacyRelayFile = RadrootsFileReference(
- scope: .data,
- relativePath: "settings/relay_settings.json"
+ scope: .data,
+ relativePath: "settings/relay_settings.json"
)
private static let sourceGenerationFile = RadrootsFileReference(
- scope: .data,
- relativePath: "state/source_generation_v1.json"
+ scope: .data,
+ relativePath: "state/source_generation_v1.json"
)
private let bootstrap: RadrootsConfigurationBootstrap
@@ -146,8 +146,8 @@ actor RadrootsConfigurationStore {
throw RadrootsConfigurationError.corruptStoredConfiguration
}
let upgraded = try Self.upgradeStoredConfiguration(
- stored,
- bootstrapFingerprint: bootstrapFingerprint
+ stored,
+ bootstrapFingerprint: bootstrapFingerprint
)
if upgraded.profile == profile,
Self.blossomMatchesProfile(upgraded.blossom, profile: profile),
@@ -159,12 +159,12 @@ actor RadrootsConfigurationStore {
}
} else {
selected = Self.bootstrapConfiguration(
- bootstrap,
- profile: profile,
- generation: (upgraded.generation ?? 0) + 1,
- activationState: .reconfigurationRequired,
- bootstrapFingerprint: bootstrapFingerprint,
- previousBlossomConfigFingerprint: upgraded.canonicalBlossomConfigFingerprint
+ bootstrap,
+ profile: profile,
+ generation: (upgraded.generation ?? 0) + 1,
+ activationState: .reconfigurationRequired,
+ bootstrapFingerprint: bootstrapFingerprint,
+ previousBlossomConfigFingerprint: upgraded.canonicalBlossomConfigFingerprint
?? upgraded.previousBlossomConfigFingerprint
)
try persist(selected)
@@ -175,26 +175,26 @@ actor RadrootsConfigurationStore {
}
selected = if stored.profile == profile {
StoredConfigurationV3(
- format: StoredConfigurationV3.format,
- profile: profile,
- writableRelays: stored.writableRelays,
- blossom: Self.blossomConfiguration(
- origins: stored.blossomOrigins,
- profile: profile
- ),
- generation: 1,
- activationState: .current,
- bootstrapFingerprint: bootstrapFingerprint,
- canonicalBlossomConfigFingerprint: nil,
- previousBlossomConfigFingerprint: nil
+ format: StoredConfigurationV3.format,
+ profile: profile,
+ writableRelays: stored.writableRelays,
+ blossom: Self.blossomConfiguration(
+ origins: stored.blossomOrigins,
+ profile: profile
+ ),
+ generation: 1,
+ activationState: .current,
+ bootstrapFingerprint: bootstrapFingerprint,
+ canonicalBlossomConfigFingerprint: nil,
+ previousBlossomConfigFingerprint: nil
)
} else {
Self.bootstrapConfiguration(
- bootstrap,
- profile: profile,
- generation: 1,
- activationState: .reconfigurationRequired,
- bootstrapFingerprint: bootstrapFingerprint
+ bootstrap,
+ profile: profile,
+ generation: 1,
+ activationState: .reconfigurationRequired,
+ bootstrapFingerprint: bootstrapFingerprint
)
}
try persist(selected)
@@ -203,34 +203,34 @@ actor RadrootsConfigurationStore {
throw RadrootsConfigurationError.corruptStoredConfiguration
}
selected = StoredConfigurationV3(
- format: StoredConfigurationV3.format,
- profile: profile,
- writableRelays: legacy.relays,
- blossom: Self.blossomConfiguration(
- origins: bootstrap.blossomOrigins,
- profile: profile
- ),
- generation: 1,
- activationState: .current,
- bootstrapFingerprint: bootstrapFingerprint,
- canonicalBlossomConfigFingerprint: nil,
- previousBlossomConfigFingerprint: nil
+ format: StoredConfigurationV3.format,
+ profile: profile,
+ writableRelays: legacy.relays,
+ blossom: Self.blossomConfiguration(
+ origins: bootstrap.blossomOrigins,
+ profile: profile
+ ),
+ generation: 1,
+ activationState: .current,
+ bootstrapFingerprint: bootstrapFingerprint,
+ canonicalBlossomConfigFingerprint: nil,
+ previousBlossomConfigFingerprint: nil
)
try persist(selected)
} else {
selected = Self.bootstrapConfiguration(
- bootstrap,
- profile: profile,
- generation: 1,
- activationState: .current,
- bootstrapFingerprint: bootstrapFingerprint
+ bootstrap,
+ profile: profile,
+ generation: 1,
+ activationState: .current,
+ bootstrapFingerprint: bootstrapFingerprint
)
try persist(selected)
}
let relays = try RadrootsNetworkValidator.relays(
- selected.writableRelays,
- profile: profile
+ selected.writableRelays,
+ profile: profile
)
guard !bootstrap.keychainServicePrefix.isEmpty,
!bootstrap.bundleIdentifier.isEmpty
@@ -238,21 +238,21 @@ actor RadrootsConfigurationStore {
throw RadrootsConfigurationError.missing("identity")
}
return RadrootsAppConfiguration(
- profile: profile,
- writableRelays: relays,
- blossom: selected.blossom,
- keychainServicePrefix: bootstrap.keychainServicePrefix,
- bundleIdentifier: bootstrap.bundleIdentifier,
- appMetadata: bootstrap.appMetadata,
- generation: selected.generation ?? 1,
- activationState: selected.activationState ?? .current,
- previousBlossomConfigFingerprint: selected.previousBlossomConfigFingerprint
+ profile: profile,
+ writableRelays: relays,
+ blossom: selected.blossom,
+ keychainServicePrefix: bootstrap.keychainServicePrefix,
+ bundleIdentifier: bootstrap.bundleIdentifier,
+ appMetadata: bootstrap.appMetadata,
+ generation: selected.generation ?? 1,
+ activationState: selected.activationState ?? .current,
+ previousBlossomConfigFingerprint: selected.previousBlossomConfigFingerprint
)
}
func confirmCanonicalBlossomConfiguration(
- _ canonical: RadrootsBlossomConfigurationStatus?,
- expectedGeneration: UInt64
+ _ canonical: RadrootsBlossomConfigurationStatus?,
+ expectedGeneration: UInt64
) throws {
guard let stored = try readStoredConfiguration(),
stored.format == StoredConfigurationV3.format,
@@ -279,10 +279,10 @@ actor RadrootsConfigurationStore {
throw RadrootsConfigurationError.invalid("canonical_blossom_configuration")
}
canonicalBlossom = RadrootsBlossomEndpointConfiguration(
- hostKind: hostKind,
- endpointAuthority: endpointAuthority,
- primaryOrigin: canonical.primaryOrigin,
- fallbackOrigins: canonical.fallbackOrigins
+ hostKind: hostKind,
+ endpointAuthority: endpointAuthority,
+ primaryOrigin: canonical.primaryOrigin,
+ fallbackOrigins: canonical.fallbackOrigins
)
canonicalFingerprint = canonical.configFingerprint
default:
@@ -290,15 +290,15 @@ actor RadrootsConfigurationStore {
}
try persist(
StoredConfigurationV3(
- format: StoredConfigurationV3.format,
- profile: stored.profile,
- writableRelays: stored.writableRelays,
- blossom: canonicalBlossom,
- generation: expectedGeneration,
- activationState: .current,
- bootstrapFingerprint: bootstrapFingerprint,
- canonicalBlossomConfigFingerprint: canonicalFingerprint,
- previousBlossomConfigFingerprint: nil
+ format: StoredConfigurationV3.format,
+ profile: stored.profile,
+ writableRelays: stored.writableRelays,
+ blossom: canonicalBlossom,
+ generation: expectedGeneration,
+ activationState: .current,
+ bootstrapFingerprint: bootstrapFingerprint,
+ canonicalBlossomConfigFingerprint: canonicalFingerprint,
+ previousBlossomConfigFingerprint: nil
)
)
}
@@ -313,15 +313,15 @@ actor RadrootsConfigurationStore {
}
try persist(
StoredConfigurationV3(
- format: stored.format,
- profile: stored.profile,
- writableRelays: stored.writableRelays,
- blossom: stored.blossom,
- generation: expectedGeneration,
- activationState: .current,
- bootstrapFingerprint: bootstrapFingerprint,
- canonicalBlossomConfigFingerprint: nil,
- previousBlossomConfigFingerprint: nil
+ format: stored.format,
+ profile: stored.profile,
+ writableRelays: stored.writableRelays,
+ blossom: stored.blossom,
+ generation: expectedGeneration,
+ activationState: .current,
+ bootstrapFingerprint: bootstrapFingerprint,
+ canonicalBlossomConfigFingerprint: nil,
+ previousBlossomConfigFingerprint: nil
)
)
}
@@ -344,9 +344,9 @@ actor RadrootsConfigurationStore {
throw RadrootsConfigurationError.persistenceFailed
}
let value = RadrootsSourceGeneration(
- schemaVersion: 1,
- generationHex: bytes.map { String(format: "%02x", $0) }.joined(),
- createdAtUnixMilliseconds: max(1, UInt64(Date().timeIntervalSince1970 * 1000))
+ schemaVersion: 1,
+ generationHex: bytes.map { String(format: "%02x", $0) }.joined(),
+ createdAtUnixMilliseconds: max(1, UInt64(Date().timeIntervalSince1970 * 1000))
)
do {
let data = try JSONEncoder.radroots.encode(value)
@@ -417,29 +417,29 @@ actor RadrootsConfigurationStore {
}
private static func bootstrapConfiguration(
- _ bootstrap: RadrootsConfigurationBootstrap,
- profile: RadrootsAppNetworkProfile,
- generation: UInt64,
- activationState: RadrootsConfigurationActivationState,
- bootstrapFingerprint: String,
- previousBlossomConfigFingerprint: String? = nil
+ _ bootstrap: RadrootsConfigurationBootstrap,
+ profile: RadrootsAppNetworkProfile,
+ generation: UInt64,
+ activationState: RadrootsConfigurationActivationState,
+ bootstrapFingerprint: String,
+ previousBlossomConfigFingerprint: String? = nil
) -> StoredConfigurationV3 {
StoredConfigurationV3(
- format: StoredConfigurationV3.format,
- profile: profile,
- writableRelays: bootstrap.relayURLs,
- blossom: blossomConfiguration(origins: bootstrap.blossomOrigins, profile: profile),
- generation: generation,
- activationState: activationState,
- bootstrapFingerprint: bootstrapFingerprint,
- canonicalBlossomConfigFingerprint: nil,
- previousBlossomConfigFingerprint: previousBlossomConfigFingerprint
+ format: StoredConfigurationV3.format,
+ profile: profile,
+ writableRelays: bootstrap.relayURLs,
+ blossom: blossomConfiguration(origins: bootstrap.blossomOrigins, profile: profile),
+ generation: generation,
+ activationState: activationState,
+ bootstrapFingerprint: bootstrapFingerprint,
+ canonicalBlossomConfigFingerprint: nil,
+ previousBlossomConfigFingerprint: previousBlossomConfigFingerprint
)
}
private static func upgradeStoredConfiguration(
- _ stored: StoredConfigurationV3,
- bootstrapFingerprint: String
+ _ stored: StoredConfigurationV3,
+ bootstrapFingerprint: String
) throws -> StoredConfigurationV3 {
if let generation = stored.generation,
let activationState = stored.activationState,
@@ -453,15 +453,15 @@ actor RadrootsConfigurationStore {
throw RadrootsConfigurationError.corruptStoredConfiguration
}
return StoredConfigurationV3(
- format: stored.format,
- profile: stored.profile,
- writableRelays: stored.writableRelays,
- blossom: stored.blossom,
- generation: generation,
- activationState: activationState,
- bootstrapFingerprint: persistedBootstrapFingerprint,
- canonicalBlossomConfigFingerprint: stored.canonicalBlossomConfigFingerprint,
- previousBlossomConfigFingerprint: stored.previousBlossomConfigFingerprint
+ format: stored.format,
+ profile: stored.profile,
+ writableRelays: stored.writableRelays,
+ blossom: stored.blossom,
+ generation: generation,
+ activationState: activationState,
+ bootstrapFingerprint: persistedBootstrapFingerprint,
+ canonicalBlossomConfigFingerprint: stored.canonicalBlossomConfigFingerprint,
+ previousBlossomConfigFingerprint: stored.previousBlossomConfigFingerprint
)
}
guard stored.generation == nil,
@@ -473,28 +473,28 @@ actor RadrootsConfigurationStore {
throw RadrootsConfigurationError.corruptStoredConfiguration
}
return StoredConfigurationV3(
- format: stored.format,
- profile: stored.profile,
- writableRelays: stored.writableRelays,
- blossom: stored.blossom,
- generation: 1,
- activationState: .current,
- bootstrapFingerprint: bootstrapFingerprint,
- canonicalBlossomConfigFingerprint: nil,
- previousBlossomConfigFingerprint: nil
+ format: stored.format,
+ profile: stored.profile,
+ writableRelays: stored.writableRelays,
+ blossom: stored.blossom,
+ generation: 1,
+ activationState: .current,
+ bootstrapFingerprint: bootstrapFingerprint,
+ canonicalBlossomConfigFingerprint: nil,
+ previousBlossomConfigFingerprint: nil
)
}
private static func bootstrapFingerprint(
- _ bootstrap: RadrootsConfigurationBootstrap,
- profile: RadrootsAppNetworkProfile
+ _ bootstrap: RadrootsConfigurationBootstrap,
+ profile: RadrootsAppNetworkProfile
) throws -> String {
do {
let data = try JSONEncoder.radroots.encode(
BootstrapNetworkIdentity(
- profile: profile,
- relayURLs: bootstrap.relayURLs,
- blossomOrigins: bootstrap.blossomOrigins
+ profile: profile,
+ relayURLs: bootstrap.relayURLs,
+ blossomOrigins: bootstrap.blossomOrigins
)
)
return SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined()
@@ -510,22 +510,22 @@ actor RadrootsConfigurationStore {
}
private static func blossomConfiguration(
- origins: [String],
- profile: RadrootsAppNetworkProfile
+ origins: [String],
+ profile: RadrootsAppNetworkProfile
) -> RadrootsBlossomEndpointConfiguration? {
guard let primary = origins.first else { return nil }
let identity = blossomIdentity(profile: profile)
return RadrootsBlossomEndpointConfiguration(
- hostKind: identity.hostKind,
- endpointAuthority: identity.endpointAuthority,
- primaryOrigin: primary,
- fallbackOrigins: Array(origins.dropFirst())
+ hostKind: identity.hostKind,
+ endpointAuthority: identity.endpointAuthority,
+ primaryOrigin: primary,
+ fallbackOrigins: Array(origins.dropFirst())
)
}
private static func blossomMatchesProfile(
- _ configuration: RadrootsBlossomEndpointConfiguration?,
- profile: RadrootsAppNetworkProfile
+ _ configuration: RadrootsBlossomEndpointConfiguration?,
+ profile: RadrootsAppNetworkProfile
) -> Bool {
guard let configuration else { return true }
let identity = blossomIdentity(profile: profile)
@@ -536,8 +536,8 @@ actor RadrootsConfigurationStore {
private static func blossomIdentity(
profile: RadrootsAppNetworkProfile
) -> (
- hostKind: RadrootsBlossomHostKind,
- endpointAuthority: RadrootsBlossomEndpointAuthority
+ hostKind: RadrootsBlossomHostKind,
+ endpointAuthority: RadrootsBlossomEndpointAuthority
) {
switch profile {
case .publicNetwork:
@@ -554,8 +554,8 @@ enum RadrootsNetworkValidator {
static let canonicalRelay = "wss://radroots.org"
static func relays(
- _ values: [String],
- profile: RadrootsAppNetworkProfile
+ _ values: [String],
+ profile: RadrootsAppNetworkProfile
) throws -> [String] {
var output: [String] = []
var seen = Set<String>()
@@ -580,8 +580,8 @@ enum RadrootsNetworkValidator {
}
private static func relay(
- _ raw: String,
- profile: RadrootsAppNetworkProfile
+ _ raw: String,
+ profile: RadrootsAppNetworkProfile
) throws -> String {
guard raw == raw.trimmingCharacters(in: .whitespacesAndNewlines),
raw.utf8.count <= 2048,
@@ -621,8 +621,8 @@ enum RadrootsNetworkValidator {
}
private static func hostAllowed(
- _ host: String,
- profile: RadrootsAppNetworkProfile
+ _ host: String,
+ profile: RadrootsAppNetworkProfile
) -> Bool {
if profile == .simulator {
return host == "localhost" || host == "127.0.0.1" || host == "::1"
diff --git a/Radroots/State/RadrootsIdentityStore.swift b/Radroots/State/RadrootsIdentityStore.swift
@@ -65,10 +65,10 @@ actor RadrootsIdentityStore {
private let userDefaults: UserDefaults
init(
- custody: RadrootsIdentityCustody,
- secureStore: any RadrootsSecureStore,
- servicePrefix: String,
- userDefaults: UserDefaults = .standard
+ custody: RadrootsIdentityCustody,
+ secureStore: any RadrootsSecureStore,
+ servicePrefix: String,
+ userDefaults: UserDefaults = .standard
) {
self.custody = custody
self.secureStore = secureStore
@@ -78,9 +78,9 @@ actor RadrootsIdentityStore {
@MainActor
static func production(
- servicePrefix: String,
- protectedDataAvailable: @escaping @Sendable () -> Bool,
- qualification: RadrootsRemoteQualificationEnvironment? = nil
+ servicePrefix: String,
+ protectedDataAvailable: @escaping @Sendable () -> Bool,
+ qualification: RadrootsRemoteQualificationEnvironment? = nil
) throws -> RadrootsIdentityStore {
let namespace = "radroots_identity_v1"
let secureStore = RadrootsAppleKeychainSecureStore(servicePrefix: servicePrefix)
@@ -89,8 +89,8 @@ actor RadrootsIdentityStore {
#if DEBUG
if qualification != nil {
configuration = try RadrootsIdentityCustodyConfiguration(
- namespace: namespace,
- secretPolicy: .secureLocalSecret
+ namespace: namespace,
+ secretPolicy: .secureLocalSecret
)
userPresence = RadrootsRemoteQualificationUserPresence()
} else {
@@ -103,22 +103,22 @@ actor RadrootsIdentityStore {
userPresence = RadrootsAppleUserPresence()
#endif
let custody = try RadrootsIdentityCustody(
- configuration: configuration,
- secureStore: secureStore,
- metadataStore: RadrootsAppleIdentityMetadataStore(
- namespace: namespace,
- keyPrefix: qualification?.identityMetadataKeyPrefix
+ configuration: configuration,
+ secureStore: secureStore,
+ metadataStore: RadrootsAppleIdentityMetadataStore(
+ namespace: namespace,
+ keyPrefix: qualification?.identityMetadataKeyPrefix
?? "org.radroots.ios.identity"
- ),
- userPresence: userPresence,
- protectedData: RadrootsProtectedDataProvider {
+ ),
+ userPresence: userPresence,
+ protectedData: RadrootsProtectedDataProvider {
protectedDataAvailable() ? .available : .unavailable
}
)
return RadrootsIdentityStore(
- custody: custody,
- secureStore: secureStore,
- servicePrefix: servicePrefix
+ custody: custody,
+ secureStore: secureStore,
+ servicePrefix: servicePrefix
)
}
@@ -129,8 +129,8 @@ actor RadrootsIdentityStore {
}
let legacySecretKey = RadrootsSecureStoreKey(
- namespace: "nostr_identity",
- name: "selected_secret_hex"
+ namespace: "nostr_identity",
+ name: "selected_secret_hex"
)
let hasLegacySecret: Bool
do {
@@ -146,25 +146,25 @@ actor RadrootsIdentityStore {
throw RadrootsIdentityStoreError.corruptLegacyMetadata
}
return RadrootsAppIdentity(
- state: .recoveryRequired,
- identityHandle: nil,
- publicKeyHex: legacyMetadata?.publicKeyHex.lowercased(),
- label: legacyMetadata?.label,
- signerGeneration: nil,
- recoveryCode: "identity.legacy_migration_required"
+ state: .recoveryRequired,
+ identityHandle: nil,
+ publicKeyHex: legacyMetadata?.publicKeyHex.lowercased(),
+ label: legacyMetadata?.label,
+ signerGeneration: nil,
+ recoveryCode: "identity.legacy_migration_required"
)
}
private func migrateLegacyIdentity() async throws -> RadrootsAppIdentity {
let legacySecretKey = RadrootsSecureStoreKey(
- namespace: "nostr_identity",
- name: "selected_secret_hex"
+ namespace: "nostr_identity",
+ name: "selected_secret_hex"
)
let legacyMetadata = try loadLegacyMetadata()
do {
let migrated = try await custody.migrateLegacyIdentity(
- from: legacySecretKey,
- label: legacyMetadata?.label
+ from: legacySecretKey,
+ label: legacyMetadata?.label
)
if let metadata = legacyMetadata,
metadata.publicKeyHex.lowercased() != migrated.identity?.publicKeyHex
@@ -187,8 +187,8 @@ actor RadrootsIdentityStore {
}
func importIdentity(
- _ material: RadrootsIdentitySecretMaterial,
- label: String? = nil
+ _ material: RadrootsIdentitySecretMaterial,
+ label: String? = nil
) async throws -> RadrootsAppIdentity {
try await custody.importIdentity(material, label: label).appValue
}
@@ -205,8 +205,8 @@ actor RadrootsIdentityStore {
let snapshot = await custody.snapshot()
if snapshot.state == .absent {
let legacyKey = RadrootsSecureStoreKey(
- namespace: "nostr_identity",
- name: "selected_secret_hex"
+ namespace: "nostr_identity",
+ name: "selected_secret_hex"
)
if try secureStore.contains(legacyKey) {
return try await migrateLegacyIdentity()
@@ -227,9 +227,9 @@ actor RadrootsIdentityStore {
throw RadrootsIdentityStoreError.unavailable
}
return RadrootsAppleCustodySigner(
- custody: custody,
- signerHandle: signerHandle,
- publicKeyHex: publicKeyHex
+ custody: custody,
+ signerHandle: signerHandle,
+ publicKeyHex: publicKeyHex
)
}
@@ -253,12 +253,12 @@ actor RadrootsIdentityStore {
private static func appIdentity(_ snapshot: RadrootsIdentitySnapshot) -> RadrootsAppIdentity {
RadrootsAppIdentity(
- state: snapshot.state.appValue,
- identityHandle: snapshot.identity?.identityHandle,
- publicKeyHex: snapshot.identity?.publicKeyHex,
- label: snapshot.identity?.label,
- signerGeneration: snapshot.signerHandle,
- recoveryCode: snapshot.recoveryCode
+ state: snapshot.state.appValue,
+ identityHandle: snapshot.identity?.identityHandle,
+ publicKeyHex: snapshot.identity?.publicKeyHex,
+ label: snapshot.identity?.label,
+ signerGeneration: snapshot.signerHandle,
+ recoveryCode: snapshot.recoveryCode
)
}
}
@@ -295,12 +295,12 @@ private final class RadrootsAppleCustodySigner: RadrootsRuntimeSigner, @unchecke
do {
let result = try await custody.sign(
RadrootsOpaqueSignRequest(
- operationID: request.operationID,
- signerHandle: signerHandle,
- publicKeyHex: request.publicKeyHex,
- digest: request.digest,
- purpose: request.purpose.appleValue,
- deadlineUnixMilliseconds: request.deadlineUnixMilliseconds
+ operationID: request.operationID,
+ signerHandle: signerHandle,
+ publicKeyHex: request.publicKeyHex,
+ digest: request.digest,
+ purpose: request.purpose.appleValue,
+ deadlineUnixMilliseconds: request.deadlineUnixMilliseconds
)
)
return .signed(signatureHex: result.signature.map { String(format: "%02x", $0) }.joined())
@@ -328,12 +328,12 @@ private final class RadrootsAppleCustodySigner: RadrootsRuntimeSigner, @unchecke
private extension RadrootsIdentitySnapshot {
var appValue: RadrootsAppIdentity {
RadrootsAppIdentity(
- state: state.appValue,
- identityHandle: identity?.identityHandle,
- publicKeyHex: identity?.publicKeyHex,
- label: identity?.label,
- signerGeneration: signerHandle,
- recoveryCode: recoveryCode
+ state: state.appValue,
+ identityHandle: identity?.identityHandle,
+ publicKeyHex: identity?.publicKeyHex,
+ label: identity?.label,
+ signerGeneration: signerHandle,
+ recoveryCode: recoveryCode
)
}
}
diff --git a/Radroots/State/RadrootsMediaStore.swift b/Radroots/State/RadrootsMediaStore.swift
@@ -153,7 +153,7 @@ final class RadrootsMediaStore: ObservableObject {
private static func failureState(_ error: Error) -> RadrootsMediaPresentationState {
let failure: RadrootsRuntimeFailure? =
- if case RadrootsRuntimeClientError.support(let value) = error {
+ if case let RadrootsRuntimeClientError.support(value) = error {
value
} else {
error as? RadrootsRuntimeFailure
diff --git a/Radroots/State/RadrootsSessionStore.swift b/Radroots/State/RadrootsSessionStore.swift
@@ -77,8 +77,8 @@ actor RadrootsSessionStore {
?? requiredString(
"RADROOTS_FIELD_IOS_KEYCHAIN_SERVICE_PREFIX", bundle: bundle
)
- let bootstrap = RadrootsConfigurationBootstrap(
- runtimeMode: try qualification?.runtimeMode
+ let bootstrap = try RadrootsConfigurationBootstrap(
+ runtimeMode: qualification?.runtimeMode
?? requiredString("RADROOTS_FIELD_IOS_RUNTIME_MODE", bundle: bundle),
relayURLs: qualification?.relayURLs
?? array("RADROOTS_FIELD_IOS_NOSTR_RELAY_URLS", bundle: bundle),
diff --git a/Radroots/State/RadrootsSupportingStores.swift b/Radroots/State/RadrootsSupportingStores.swift
@@ -20,8 +20,8 @@ final class RadrootsSearchStore: ObservableObject {
private var generation: UInt64 = 0
init(
- runtimeClient: RadrootsRuntimeClient,
- now: @escaping @Sendable () -> UInt64 = { UInt64(Date().timeIntervalSince1970) }
+ runtimeClient: RadrootsRuntimeClient,
+ now: @escaping @Sendable () -> UInt64 = { UInt64(Date().timeIntervalSince1970) }
) {
self.runtimeClient = runtimeClient
self.now = now
@@ -65,10 +65,10 @@ final class RadrootsSearchStore: ObservableObject {
state = .loading
do {
let loaded = try await runtimeClient.search(
- context: context,
- query: normalized,
- limit: 50,
- asOfUnixSeconds: now()
+ context: context,
+ query: normalized,
+ limit: 50,
+ asOfUnixSeconds: now()
)
guard requestedGeneration == generation, !Task.isCancelled else { return }
results = Self.unique(loaded)
@@ -114,9 +114,9 @@ final class RadrootsMeStore: ObservableObject {
private var observationGeneration: UInt64 = 0
init(
- runtimeClient: RadrootsRuntimeClient,
- now: @escaping @Sendable () -> UInt64 = { UInt64(Date().timeIntervalSince1970) },
- observationDelay: @escaping @Sendable (UInt32) async throws -> Void =
+ runtimeClient: RadrootsRuntimeClient,
+ now: @escaping @Sendable () -> UInt64 = { UInt64(Date().timeIntervalSince1970) },
+ observationDelay: @escaping @Sendable (UInt32) async throws -> Void =
RadrootsRuntimeObservationBackoff.sleep
) {
self.runtimeClient = runtimeClient
@@ -161,8 +161,8 @@ final class RadrootsMeStore: ObservableObject {
}
do {
let loaded = try await runtimeClient.me(
- context: context,
- asOfUnixSeconds: now()
+ context: context,
+ asOfUnixSeconds: now()
)
guard requestedGeneration == generation, !Task.isCancelled else { return }
snapshot = loaded
@@ -318,25 +318,25 @@ final class RadrootsSettingsStore: ObservableObject {
do {
let transition = try await runtimeClient.replaceMobileSettings(
input: RadrootsReplaceSettings(
- expectedRevision: settings.revision,
- networkEnvironment: networkEnvironment,
- relays: relays,
- blossomAuthority: blossomAuthority,
- blossomPrimaryOrigin: blossomPrimaryOrigin,
- blossomFallbackOrigins: fallbacks,
- allowCellularDownloads: allowCellularDownloads,
- allowCellularUploads: allowCellularUploads,
- allowBackgroundTransfers: allowBackgroundTransfers,
- mediaCacheBytes: UInt64(max(mediaCacheMegabytes, 1)) * 1_048_576,
- mediaCacheArtifacts: UInt32(max(mediaCacheArtifacts, 1))
+ expectedRevision: settings.revision,
+ networkEnvironment: networkEnvironment,
+ relays: relays,
+ blossomAuthority: blossomAuthority,
+ blossomPrimaryOrigin: blossomPrimaryOrigin,
+ blossomFallbackOrigins: fallbacks,
+ allowCellularDownloads: allowCellularDownloads,
+ allowCellularUploads: allowCellularUploads,
+ allowBackgroundTransfers: allowBackgroundTransfers,
+ mediaCacheBytes: UInt64(max(mediaCacheMegabytes, 1)) * 1_048_576,
+ mediaCacheArtifacts: UInt32(max(mediaCacheArtifacts, 1))
)
)
guard requestedGeneration == generation, !Task.isCancelled else { return false }
apply(transition.settings)
let effects = [
- transition.runtimeRestartRequired ? "runtime restart" : nil,
- transition.outboxRequeueRequired ? "outbox requeue" : nil,
- transition.mediaCacheInvalidationRequired ? "media cache refresh" : nil,
+ transition.runtimeRestartRequired ? "runtime restart" : nil,
+ transition.outboxRequeueRequired ? "outbox requeue" : nil,
+ transition.mediaCacheInvalidationRequired ? "media cache refresh" : nil,
].compactMap(\.self)
message = effects.isEmpty
? "Settings saved."
@@ -362,13 +362,13 @@ final class RadrootsSettingsStore: ObservableObject {
do {
let status = try await runtimeClient.saveProfileMetadata(
input: RadrootsProfileMetadataInput(
- name: profileName,
- displayName: optional(profileDisplayName),
- about: optional(profileAbout),
- picture: nil,
- banner: nil,
- nip05: optional(profileNip05),
- bot: profileBot
+ name: profileName,
+ displayName: optional(profileDisplayName),
+ about: optional(profileAbout),
+ picture: nil,
+ banner: nil,
+ nip05: optional(profileNip05),
+ bot: profileBot
)
)
guard requestedGeneration == generation, !Task.isCancelled else { return }
@@ -392,8 +392,8 @@ final class RadrootsSettingsStore: ObservableObject {
guard let profileStatus else { return }
await runProfileOperation {
try await self.runtimeClient.cancelProfile(
- operationID: profileStatus.id,
- expectedRevision: profileStatus.revision
+ operationID: profileStatus.id,
+ expectedRevision: profileStatus.revision
)
}
}
diff --git a/Radroots/State/RadrootsTodayStore.swift b/Radroots/State/RadrootsTodayStore.swift
@@ -31,14 +31,14 @@ final class RadrootsTodayStore: ObservableObject {
private var observationGeneration: UInt64 = 0
init(
- runtimeClient: RadrootsRuntimeClient,
- contexts: [RadrootsLocalNetwork] = [],
- selectedContextID: String? = nil,
- pageSize: UInt16 = 20,
- now: @escaping @Sendable () -> UInt64 = {
+ runtimeClient: RadrootsRuntimeClient,
+ contexts: [RadrootsLocalNetwork] = [],
+ selectedContextID: String? = nil,
+ pageSize: UInt16 = 20,
+ now: @escaping @Sendable () -> UInt64 = {
UInt64(Date().timeIntervalSince1970)
},
- observationDelay: @escaping @Sendable (UInt32) async throws -> Void =
+ observationDelay: @escaping @Sendable (UInt32) async throws -> Void =
RadrootsRuntimeObservationBackoff.sleep
) {
self.runtimeClient = runtimeClient
@@ -128,8 +128,8 @@ final class RadrootsTodayStore: ObservableObject {
}
func reload(
- refreshProjection: Bool = true,
- update: RadrootsTodayProjectionUpdate = .incremental
+ refreshProjection: Bool = true,
+ update: RadrootsTodayProjectionUpdate = .incremental
) async {
guard let context = selectedContext else {
cards = []
@@ -150,9 +150,9 @@ final class RadrootsTodayStore: ObservableObject {
if refreshProjection {
do {
_ = try await runtimeClient.refreshToday(
- context: context,
- nowUnixSeconds: now(),
- update: update
+ context: context,
+ nowUnixSeconds: now(),
+ update: update
)
} catch {
refreshFailure = error
@@ -163,9 +163,9 @@ final class RadrootsTodayStore: ObservableObject {
let asOf = now()
let page = try await runtimeClient.todayPage(
request: .first(
- context: context,
- limit: pageSize,
- asOfUnixSeconds: asOf
+ context: context,
+ limit: pageSize,
+ asOfUnixSeconds: asOf
)
)
guard generation == requestGeneration, !Task.isCancelled else { return }
diff --git a/Radroots/Views/RadrootsAddView.swift b/Radroots/Views/RadrootsAddView.swift
@@ -26,7 +26,7 @@ struct RadrootsAddView: View {
ProgressView("Loading Add…")
.accessibilityIdentifier("radroots.add.loading")
}
- case .failed(let message):
+ case let .failed(message):
Section {
Label(message, systemImage: "exclamationmark.triangle")
.foregroundStyle(.secondary)
@@ -265,7 +265,8 @@ struct RadrootsAddView: View {
.accessibilityValue(
"\(media.width) by \(media.height), "
+ ByteCountFormatter.string(
- fromByteCount: Int64(clamping: media.byteSize), countStyle: .file)
+ fromByteCount: Int64(clamping: media.byteSize), countStyle: .file
+ )
)
}
diff --git a/Radroots/Views/RadrootsSupportingViews.swift b/Radroots/Views/RadrootsSupportingViews.swift
@@ -24,7 +24,7 @@ struct RadrootsSearchSheet: View {
ProgressView("Searching…")
case .empty:
ContentUnavailableView.search(text: store.query)
- case .failed(let message):
+ case let .failed(message):
ContentUnavailableView {
Label("Search unavailable", systemImage: "exclamationmark.triangle")
} description: {
@@ -52,7 +52,7 @@ struct RadrootsSearchSheet: View {
private var resultList: some View {
List(store.results) { result in
switch (result.card, result.profile) {
- case (let card?, _):
+ case let (card?, _):
NavigationLink {
RadrootsTodayDetailView(
card: card,
@@ -68,7 +68,7 @@ struct RadrootsSearchSheet: View {
RadrootsTodayCardView(card: card, context: context, mediaStore: mediaStore)
}
.accessibilityIdentifier("radroots.search.card.\(result.id)")
- case (_, let profile?):
+ case let (_, profile?):
NavigationLink {
RadrootsProfileView(profile: profile, context: context, mediaStore: mediaStore)
} label: {
@@ -110,7 +110,7 @@ struct RadrootsMeSheet: View {
switch store.state {
case .idle, .loading:
ProgressView("Loading your profile…")
- case .failed(let message) where store.snapshot == nil:
+ case let .failed(message) where store.snapshot == nil:
ContentUnavailableView {
Label("Profile unavailable", systemImage: "person.crop.circle.badge.exclamationmark")
} description: {
@@ -408,7 +408,7 @@ struct RadrootsSettingsView: View {
TextField("Display name", text: $settingsStore.profileDisplayName)
.accessibilityIdentifier("radroots.settings.profile.display_name")
TextField("About", text: $settingsStore.profileAbout, axis: .vertical)
- .lineLimit(3...8)
+ .lineLimit(3 ... 8)
.accessibilityIdentifier("radroots.settings.profile.about")
TextField("NIP-05 identifier", text: $settingsStore.profileNip05)
.textInputAutocapitalization(.never)
@@ -488,7 +488,7 @@ struct RadrootsSettingsView: View {
text: $settingsStore.blossomFallbackOrigins,
axis: .vertical
)
- .lineLimit(2...5)
+ .lineLimit(2 ... 5)
.textInputAutocapitalization(.never)
.keyboardType(.URL)
Toggle("Cellular downloads", isOn: $settingsStore.allowCellularDownloads)
@@ -499,13 +499,13 @@ struct RadrootsSettingsView: View {
Stepper(
"Cache: \(settingsStore.mediaCacheMegabytes) MB",
value: $settingsStore.mediaCacheMegabytes,
- in: 16...2048,
+ in: 16 ... 2048,
step: 16
)
Stepper(
"Artifacts: \(settingsStore.mediaCacheArtifacts)",
value: $settingsStore.mediaCacheArtifacts,
- in: 1...10000,
+ in: 1 ... 10000,
step: 100
)
Button("Save network and storage settings") {
diff --git a/Radroots/Views/RadrootsTodayView.swift b/Radroots/Views/RadrootsTodayView.swift
@@ -24,14 +24,14 @@ struct RadrootsTodayView: View {
.accessibilityIdentifier("radroots.today.loading")
case .empty:
emptyView
- case .failed(let message) where store.cards.isEmpty:
+ case let .failed(message) where store.cards.isEmpty:
unavailableView(
title: "Today is unavailable",
message: message,
systemImage: "exclamationmark.triangle"
)
.accessibilityIdentifier("radroots.today.error")
- case .offline(let message) where store.cards.isEmpty:
+ case let .offline(message) where store.cards.isEmpty:
unavailableView(
title: "You’re offline",
message: message,
@@ -99,9 +99,9 @@ struct RadrootsTodayView: View {
private var feed: some View {
List {
- if case .offline(let message) = store.state {
+ if case let .offline(message) = store.state {
statusBanner(message: message, systemImage: "wifi.slash")
- } else if case .failed(let message) = store.state {
+ } else if case let .failed(message) = store.state {
statusBanner(message: message, systemImage: "exclamationmark.triangle")
}
@@ -375,7 +375,7 @@ struct RadrootsLocalMediaContent: View {
let state = store.state(for: media, context: context)
Group {
switch state {
- case .ready(let artifact):
+ case let .ready(artifact):
if let image = UIImage(data: artifact.bytes) {
Image(uiImage: image)
.resizable()
diff --git a/Radroots/Views/RuntimeStatusView.swift b/Radroots/Views/RuntimeStatusView.swift
@@ -196,9 +196,9 @@ private struct RadrootsSecureIdentityImportField: UIViewRepresentable {
let container = UIView()
container.addSubview(stack)
NSLayoutConstraint.activate([
- stack.leadingAnchor.constraint(equalTo: container.leadingAnchor),
- stack.trailingAnchor.constraint(equalTo: container.trailingAnchor),
- stack.topAnchor.constraint(equalTo: container.topAnchor),
+ stack.leadingAnchor.constraint(equalTo: container.leadingAnchor),
+ stack.trailingAnchor.constraint(equalTo: container.trailingAnchor),
+ stack.topAnchor.constraint(equalTo: container.topAnchor),
])
context.coordinator.field = field
context.coordinator.errorLabel = error
diff --git a/RadrootsTests/RadrootsAddStoreTests.swift b/RadrootsTests/RadrootsAddStoreTests.swift
@@ -1,9 +1,8 @@
import CryptoKit
+@testable import RadrootsApp
import RadrootsKit
import XCTest
-@testable import RadrootsApp
-
final class RadrootsAddStoreTests: XCTestCase {
func testBackgroundUploadResumesReceiptRetriesAndCompletedStateWithoutDuplicateEnqueue()
async throws
@@ -389,7 +388,7 @@ final class RadrootsAddStoreTests: XCTestCase {
let recordedRetraction = await backend.lastRetraction()
let retraction = try XCTUnwrap(recordedRetraction)
XCTAssertEqual(retraction.commandType, .createFoodAvailability)
- XCTAssertEqual(retraction.targetKind, 30_402)
+ XCTAssertEqual(retraction.targetKind, 30402)
XCTAssertEqual(retraction.targetCardID, String(repeating: "c", count: 64))
XCTAssertEqual(retraction.targetEventID, String(repeating: "e", count: 64))
XCTAssertEqual(retraction.targetAddress, Self.card().sourceAddress)
@@ -545,7 +544,7 @@ final class RadrootsAddStoreTests: XCTestCase {
await store.start()
store.selectType(.createPhotoUpdate)
- for _ in 0..<20 {
+ for _ in 0 ..< 20 {
await store.importPhotos()
}
XCTAssertEqual(store.form.media.count, 20)
@@ -664,8 +663,10 @@ final class RadrootsAddStoreTests: XCTestCase {
private static func waitUntil(
_ predicate: @escaping @Sendable () async -> Bool
) async -> Bool {
- for _ in 0..<1_000 {
- if await predicate() { return true }
+ for _ in 0 ..< 1000 {
+ if await predicate() {
+ return true
+ }
try? await Task.sleep(nanoseconds: 1_000_000)
}
return false
@@ -897,7 +898,7 @@ private actor AddBackend: RadrootsRuntimeBackend {
writeLastAttemptUnixMilliseconds: nil,
readNextAttemptUnixMilliseconds: nil,
writeNextAttemptUnixMilliseconds: nil
- )
+ ),
] : []
),
blossomConfiguration: RadrootsBlossomConfigurationStatus(
@@ -965,14 +966,14 @@ private actor AddBackend: RadrootsRuntimeBackend {
schemaVersion: 1,
commandType: .createUpdate,
label: "Update",
- fields: [text("content", "Update", .multilineText, true, 65_535)]
+ fields: [text("content", "Update", .multilineText, true, 65535)]
),
RadrootsAddSchema(
schemaVersion: 1,
commandType: .createPhotoUpdate,
label: "Photo update",
fields: [
- text("content", "Update", .multilineText, true, 65_535),
+ text("content", "Update", .multilineText, true, 65535),
media(true, 20),
]
),
@@ -981,7 +982,7 @@ private actor AddBackend: RadrootsRuntimeBackend {
commandType: .createAsk,
label: "Ask",
fields: [
- text("content", "Question", .multilineText, true, 65_535),
+ text("content", "Question", .multilineText, true, 65535),
media(false, 20),
]
),
@@ -992,7 +993,7 @@ private actor AddBackend: RadrootsRuntimeBackend {
fields: [
text("identifier", "Identifier", .text, true, 256),
text("title", "Title", .text, true, 256),
- text("content", "Description", .multilineText, false, 65_535),
+ text("content", "Description", .multilineText, false, 65535),
text("event_start", "Starts", .dateTime, true, nil),
text("event_end", "Ends", .dateTime, false, nil),
text("location", "Location", .location, false, 256),
@@ -1007,7 +1008,7 @@ private actor AddBackend: RadrootsRuntimeBackend {
text("identifier", "Identifier", .text, true, 256),
text("title", "Food", .text, true, 256),
text("summary", "Summary", .text, true, 256),
- text("content", "Details", .multilineText, true, 65_535),
+ text("content", "Details", .multilineText, true, 65535),
text("location", "Pickup location", .location, true, 256),
text("price_amount", "Price", .decimal, true, 64),
field("currency", "Currency", .choice, true, [], 3, nil),
@@ -1213,7 +1214,8 @@ private actor AddBackend: RadrootsRuntimeBackend {
guard current.revision == expectedRevision else { throw unsupported() }
let value = replacing(
current, revision: current.revision + 1, state: .queued,
- updatedAt: current.updatedAtUnixMilliseconds + 1)
+ updatedAt: current.updatedAtUnixMilliseconds + 1
+ )
values[id] = value
return value
}
@@ -1222,7 +1224,8 @@ private actor AddBackend: RadrootsRuntimeBackend {
let current = try draftStatus(id: id)
let value = replacing(
current, revision: current.revision + 1, state: .queued,
- updatedAt: current.updatedAtUnixMilliseconds + 1)
+ updatedAt: current.updatedAtUnixMilliseconds + 1
+ )
values[id] = value
return value
}
@@ -1334,7 +1337,8 @@ private actor AddBackend: RadrootsRuntimeBackend {
let current = try storedDraft(id: id)
let value = replacing(
current, revision: expectedRevision, state: .complete,
- updatedAt: current.updatedAtUnixMilliseconds)
+ updatedAt: current.updatedAtUnixMilliseconds
+ )
values[id] = value
return value
}
@@ -1346,7 +1350,8 @@ private actor AddBackend: RadrootsRuntimeBackend {
let current = try draftStatus(id: id)
let value = replacing(
current, revision: current.revision + 1, state: .cancelled,
- updatedAt: current.updatedAtUnixMilliseconds + 1)
+ updatedAt: current.updatedAtUnixMilliseconds + 1
+ )
values[id] = value
return value
}
@@ -1451,7 +1456,8 @@ private actor AddBackend: RadrootsRuntimeBackend {
private func unsupported() -> RadrootsRuntimeFailure {
.local(
- operation: "test.add", code: "test.unsupported", safeMessage: "Unsupported test operation.")
+ operation: "test.add", code: "test.unsupported", safeMessage: "Unsupported test operation."
+ )
}
}
@@ -1544,7 +1550,7 @@ private final class BackgroundUploadFixture: @unchecked Sendable {
possibleOrphan: false,
orphanReasonCode: nil,
orphanRecordedAtUnixMilliseconds: nil
- )
+ ),
],
settlement: nil,
isRevision: false
@@ -1698,7 +1704,7 @@ private actor BackgroundTransferHarness: RadrootsBackgroundTransfer {
case .accepted:
acceptedSettlementCount += 1
values[identifier] = try snapshot(request: value.request, state: .completed)
- case .rejected(let code):
+ case let .rejected(code):
values[identifier] = try RadrootsBackgroundTransferSnapshot(
request: value.request,
state: .failed,
diff --git a/RadrootsTests/RadrootsLifecycleTests.swift b/RadrootsTests/RadrootsLifecycleTests.swift
@@ -1,9 +1,8 @@
import Foundation
+@testable import RadrootsApp
import RadrootsKit
import XCTest
-@testable import RadrootsApp
-
final class RadrootsLifecycleTests: XCTestCase {
func testBackgroundEventsCompleteExactlyOnceBeforeAndAfterAttachment() async {
let router = RadrootsBackgroundEventRouter()
@@ -31,7 +30,7 @@ final class RadrootsLifecycleTests: XCTestCase {
func testBackgroundEventQueueIsBoundedAndShutdownCompletesPendingEvents() async {
let router = RadrootsBackgroundEventRouter()
- let probes = (0..<12).map { _ in CompletionProbe() }
+ let probes = (0 ..< 12).map { _ in CompletionProbe() }
for probe in probes {
await router.handle(
@@ -73,7 +72,7 @@ final class RadrootsLifecycleTests: XCTestCase {
let privateKey = String(repeating: "ab", count: 32)
let privatePath = "/Users/private/radroots/private.json"
- for index in 0..<40 {
+ for index in 0 ..< 40 {
await coordinator.record(
"ios.lifecycle.test_event",
fields: [
@@ -111,7 +110,8 @@ final class RadrootsLifecycleTests: XCTestCase {
private func makeRoots() throws -> RadrootsAppleFileRoots {
let base = FileManager.default.temporaryDirectory
.appendingPathComponent(
- "radroots-ios-lifecycle-tests-\(UUID().uuidString)", isDirectory: true)
+ "radroots-ios-lifecycle-tests-\(UUID().uuidString)", isDirectory: true
+ )
return try RadrootsAppleFileRoots(
appIdentifier: "org.radroots.tests",
dataRoot: base.appendingPathComponent("data", isDirectory: true),
@@ -141,7 +141,7 @@ final class RadrootsLifecycleTests: XCTestCase {
writeLastAttemptUnixMilliseconds: nil,
readNextAttemptUnixMilliseconds: nil,
writeNextAttemptUnixMilliseconds: nil
- )
+ ),
]
),
blossomConfiguration: nil,
diff --git a/RadrootsTests/RadrootsMediaStoreTests.swift b/RadrootsTests/RadrootsMediaStoreTests.swift
@@ -44,14 +44,14 @@ final class RadrootsMediaStoreTests: XCTestCase {
func testRetryableRetrievalFailurePresentsOfflineState() async throws {
let backend = MediaBackend(
retrievalFailure: RadrootsRuntimeFailure(
- schemaVersion: 1,
- code: "media_network_offline",
- category: "network",
- retryable: true,
- recoveryActions: ["retry"],
- operationID: "test.media.retrieve",
- capabilityID: "blossom_source",
- safeMessage: "The photo service is offline."
+ schemaVersion: 1,
+ code: "media_network_offline",
+ category: "network",
+ retryable: true,
+ recoveryActions: ["retry"],
+ operationID: "test.media.retrieve",
+ capabilityID: "blossom_source",
+ safeMessage: "The photo service is offline."
)
)
let client = try await startedClient(backend: backend)
@@ -95,8 +95,8 @@ final class RadrootsMediaStoreTests: XCTestCase {
@MainActor
private func waitUntil(
- attempts: Int = 100,
- condition: () -> Bool
+ attempts: Int = 100,
+ condition: () -> Bool
) async {
for _ in 0 ..< attempts {
if condition() {
@@ -110,40 +110,40 @@ final class RadrootsMediaStoreTests: XCTestCase {
private func makeConfiguration() -> RadrootsRuntimeLaunchConfiguration {
RadrootsRuntimeLaunchConfiguration(
- applicationSupportDirectory: "/tmp/radroots-media-tests",
- publicKeyHex: String(repeating: "ab", count: 32),
- sourceGenerationHex: String(repeating: "cd", count: 32),
- sourceGenerationCreatedAtUnixMilliseconds: 1,
- protectedData: .available,
- networkProfile: .simulator,
- writableRelays: ["ws://127.0.0.1:21000"],
- blossom: RadrootsBlossomEndpointConfiguration(
- hostKind: .simulator,
- endpointAuthority: .loopbackDevelopment,
- primaryOrigin: "http://127.0.0.1:21100",
- fallbackOrigins: []
- ),
- app: RadrootsRuntimeAppMetadata(
- bundleIdentifier: "org.radroots.media-tests",
- version: "0.1.0-alpha",
- buildNumber: "1",
- buildSHA: nil
- ),
- signerGeneration: "media-tests",
- signer: MediaTestSigner(),
- adoptBootstrapSettings: false
+ applicationSupportDirectory: "/tmp/radroots-media-tests",
+ publicKeyHex: String(repeating: "ab", count: 32),
+ sourceGenerationHex: String(repeating: "cd", count: 32),
+ sourceGenerationCreatedAtUnixMilliseconds: 1,
+ protectedData: .available,
+ networkProfile: .simulator,
+ writableRelays: ["ws://127.0.0.1:21000"],
+ blossom: RadrootsBlossomEndpointConfiguration(
+ hostKind: .simulator,
+ endpointAuthority: .loopbackDevelopment,
+ primaryOrigin: "http://127.0.0.1:21100",
+ fallbackOrigins: []
+ ),
+ app: RadrootsRuntimeAppMetadata(
+ bundleIdentifier: "org.radroots.media-tests",
+ version: "0.1.0-alpha",
+ buildNumber: "1",
+ buildSHA: nil
+ ),
+ signerGeneration: "media-tests",
+ signer: MediaTestSigner(),
+ adoptBootstrapSettings: false
)
}
private func makeContext() -> RadrootsLocalNetwork {
RadrootsLocalNetwork(
- schemaVersion: 1,
- id: "media-context",
- label: "Media context",
- relayURLs: ["ws://127.0.0.1:21000"],
- locality: nil,
- followedAuthors: [],
- generation: 1
+ schemaVersion: 1,
+ id: "media-context",
+ label: "Media context",
+ relayURLs: ["ws://127.0.0.1:21000"],
+ locality: nil,
+ followedAuthors: [],
+ generation: 1
)
}
@@ -151,15 +151,15 @@ final class RadrootsMediaStoreTests: XCTestCase {
verification: RadrootsMediaVerificationState
) -> RadrootsMediaReference {
RadrootsMediaReference(
- referenceFingerprint: String(repeating: "f", count: 64),
- url: "https://blossom.example/\(String(repeating: "a", count: 64)).png",
- sha256: String(repeating: "a", count: 64),
- mediaType: "image/png",
- width: 1,
- height: 1,
- byteSize: nil,
- alt: "A farm photo",
- verification: verification
+ referenceFingerprint: String(repeating: "f", count: 64),
+ url: "https://blossom.example/\(String(repeating: "a", count: 64)).png",
+ sha256: String(repeating: "a", count: 64),
+ mediaType: "image/png",
+ width: 1,
+ height: 1,
+ byteSize: nil,
+ alt: "A farm photo",
+ verification: verification
)
}
@@ -169,12 +169,12 @@ final class RadrootsMediaStoreTests: XCTestCase {
)!
) -> RadrootsVerifiedMediaArtifact {
RadrootsVerifiedMediaArtifact(
- artifactID: String(repeating: "a", count: 64),
- bytes: bytes,
- byteSize: UInt64(bytes.count),
- mediaType: "image/png",
- width: 1,
- height: 1
+ artifactID: String(repeating: "a", count: 64),
+ bytes: bytes,
+ byteSize: UInt64(bytes.count),
+ mediaType: "image/png",
+ width: 1,
+ height: 1
)!
}
}
@@ -199,9 +199,9 @@ private actor MediaBackend: RadrootsRuntimeBackend {
private var closed = false
init(
- cachedArtifact: RadrootsVerifiedMediaArtifact? = nil,
- retrievedArtifact: RadrootsVerifiedMediaArtifact? = nil,
- retrievalFailure: RadrootsRuntimeFailure? = nil
+ cachedArtifact: RadrootsVerifiedMediaArtifact? = nil,
+ retrievedArtifact: RadrootsVerifiedMediaArtifact? = nil,
+ retrievalFailure: RadrootsRuntimeFailure? = nil
) {
self.cachedArtifact = cachedArtifact
self.retrievedArtifact = retrievedArtifact
@@ -210,16 +210,16 @@ private actor MediaBackend: RadrootsRuntimeBackend {
func snapshot() -> RadrootsRuntimeSnapshot {
RadrootsRuntimeSnapshot(
- identity: RadrootsRuntimeIdentity(
- publicKeyHex: String(repeating: "ab", count: 32),
- hostSignerConfigured: true
- ),
- relay: nil,
- blossomConfiguration: nil,
- blossomEvidence: nil,
- crateName: "radroots_mobile_ffi",
- crateVersion: "0.1.0-alpha",
- isClosed: closed
+ identity: RadrootsRuntimeIdentity(
+ publicKeyHex: String(repeating: "ab", count: 32),
+ hostSignerConfigured: true
+ ),
+ relay: nil,
+ blossomConfiguration: nil,
+ blossomEvidence: nil,
+ crateName: "radroots_mobile_ffi",
+ crateVersion: "0.1.0-alpha",
+ isClosed: closed
)
}
@@ -228,16 +228,16 @@ private actor MediaBackend: RadrootsRuntimeBackend {
}
func refreshToday(
- context _: RadrootsLocalNetwork,
- nowUnixSeconds _: UInt64,
- update _: RadrootsTodayProjectionUpdate
+ context _: RadrootsLocalNetwork,
+ nowUnixSeconds _: UInt64,
+ update _: RadrootsTodayProjectionUpdate
) throws -> RadrootsTodayRefreshReceipt {
throw testFailure(operation: "test.media.refresh")
}
func retrieveMedia(
- context _: RadrootsLocalNetwork,
- reference _: RadrootsMediaReference
+ context _: RadrootsLocalNetwork,
+ reference _: RadrootsMediaReference
) throws -> RadrootsVerifiedMediaArtifact {
retrieveCalls += 1
if let retrievalFailure {
@@ -250,16 +250,16 @@ private actor MediaBackend: RadrootsRuntimeBackend {
}
func verifiedMediaArtifact(
- context _: RadrootsLocalNetwork,
- artifactID _: String
+ context _: RadrootsLocalNetwork,
+ artifactID _: String
) -> RadrootsVerifiedMediaArtifact? {
cachedCalls += 1
return cachedArtifact
}
func invalidateMediaArtifact(
- context _: RadrootsLocalNetwork,
- artifactID: String
+ context _: RadrootsLocalNetwork,
+ artifactID: String
) -> Bool {
invalidatedIDs.append(artifactID)
cachedArtifact = nil
@@ -267,8 +267,8 @@ private actor MediaBackend: RadrootsRuntimeBackend {
}
func subscribe(
- bufferCapacity _: Int,
- receive _: @escaping @Sendable (RadrootsRuntimeChange) async -> Void
+ bufferCapacity _: Int,
+ receive _: @escaping @Sendable (RadrootsRuntimeChange) async -> Void
) -> any RadrootsRuntimeSubscriptionToken {
MediaSubscriptionToken()
}
@@ -289,9 +289,9 @@ private actor MediaBackend: RadrootsRuntimeBackend {
private func testFailure(operation: String) -> RadrootsRuntimeFailure {
.local(
- operation: operation,
- code: "test.media.unsupported",
- safeMessage: "This test operation is unsupported."
+ operation: operation,
+ code: "test.media.unsupported",
+ safeMessage: "This test operation is unsupported."
)
}
}
diff --git a/RadrootsTests/RadrootsRemoteQualificationTests.swift b/RadrootsTests/RadrootsRemoteQualificationTests.swift
@@ -1,9 +1,8 @@
import CryptoKit
+@testable import RadrootsApp
import RadrootsKit
import XCTest
-@testable import RadrootsApp
-
final class RadrootsRemoteQualificationTests: XCTestCase {
func testQualificationIsOptInAndCarriesOnlyPublicInputs() throws {
XCTAssertNil(try RadrootsRemoteQualificationEnvironment.current(environment: [:]))
@@ -48,14 +47,14 @@ final class RadrootsRemoteQualificationTests: XCTestCase {
XCTAssertThrowsError(
try RadrootsRemoteQualificationEnvironment.current(
environment: base.merging([
- RadrootsRemoteQualificationEnvironment.runIDKey: "../../bad"
+ RadrootsRemoteQualificationEnvironment.runIDKey: "../../bad",
]) { _, new in new }
)
)
XCTAssertThrowsError(
try RadrootsRemoteQualificationEnvironment.current(
environment: base.merging([
- RadrootsRemoteQualificationEnvironment.networkProfileKey: "automatic"
+ RadrootsRemoteQualificationEnvironment.networkProfileKey: "automatic",
]) { _, new in new }
)
)
@@ -84,7 +83,7 @@ final class RadrootsRemoteQualificationTests: XCTestCase {
try RadrootsRemoteQualificationEnvironment.current(
environment: base.merging([
RadrootsRemoteQualificationEnvironment.blossomOriginsKey:
- "https://one.example,https://two.example"
+ "https://one.example,https://two.example",
]) { _, new in new }
)
)
@@ -92,7 +91,7 @@ final class RadrootsRemoteQualificationTests: XCTestCase {
try RadrootsRemoteQualificationEnvironment.current(
environment: base.merging([
RadrootsRemoteQualificationEnvironment.mediaRelativePathKey:
- "../../private-key"
+ "../../private-key",
]) { _, new in new }
)
)
@@ -139,8 +138,8 @@ final class RadrootsRemoteQualificationTests: XCTestCase {
}
func testQualificationMediaFixtureHasPinnedDigest() throws {
- let digest = SHA256.hash(
- data: try RadrootsRemoteQualificationEnvironment.mediaFixtureData()
+ let digest = try SHA256.hash(
+ data: RadrootsRemoteQualificationEnvironment.mediaFixtureData()
)
XCTAssertEqual(
digest.map { String(format: "%02x", $0) }.joined(),
diff --git a/RadrootsTests/RadrootsRootShellTests.swift b/RadrootsTests/RadrootsRootShellTests.swift
@@ -15,58 +15,58 @@ final class RadrootsRootShellTests: XCTestCase {
func testDeepLinksAcceptOnlyCanonicalRootAuthorities() throws {
for (rawValue, expected) in [
- ("radroots://today", RadrootsRootTab.today),
- ("RADROOTS://TODAY", RadrootsRootTab.today),
- ("radroots://add", RadrootsRootTab.add),
- ("RadRoots://AdD", RadrootsRootTab.add),
+ ("radroots://today", RadrootsRootTab.today),
+ ("RADROOTS://TODAY", RadrootsRootTab.today),
+ ("radroots://add", RadrootsRootTab.add),
+ ("RadRoots://AdD", RadrootsRootTab.add),
] {
XCTAssertEqual(
- try RadrootsRootTab.resolve(url: XCTUnwrap(URL(string: rawValue))),
- expected,
- "Expected canonical root deep link: \(rawValue)"
+ try RadrootsRootTab.resolve(url: XCTUnwrap(URL(string: rawValue))),
+ expected,
+ "Expected canonical root deep link: \(rawValue)"
)
}
}
func testDeepLinksRejectRemovedAndStructurallyAmbiguousRoutes() throws {
for rawValue in [
- "radroots://capture",
- "radroots://activity",
- "radroots://settings",
- "radroots://search",
- "radroots://me",
- "radroots:today",
- "radroots:/today",
- "radroots:///today",
- "radroots://host/today",
- "radroots://today/",
- "radroots://today/add",
- "radroots://add/today",
+ "radroots://capture",
+ "radroots://activity",
+ "radroots://settings",
+ "radroots://search",
+ "radroots://me",
+ "radroots:today",
+ "radroots:/today",
+ "radroots:///today",
+ "radroots://host/today",
+ "radroots://today/",
+ "radroots://today/add",
+ "radroots://add/today",
] {
XCTAssertNil(
- try RadrootsRootTab.resolve(url: XCTUnwrap(URL(string: rawValue))),
- "Expected noncanonical deep link rejection: \(rawValue)"
+ try RadrootsRootTab.resolve(url: XCTUnwrap(URL(string: rawValue))),
+ "Expected noncanonical deep link rejection: \(rawValue)"
)
}
}
- func testDeepLinksRejectAuthorityDecorationsAndEncodedAliases() throws {
+ func testDeepLinksRejectAuthorityDecorationsAndEncodedAliases() {
for rawValue in [
- "radroots://user@today",
- "radroots://user:password@add",
- "radroots://today:7447",
- "radroots://today?",
- "radroots://today?source=widget",
- "radroots://today#",
- "radroots://add#composer",
- "radroots://%74oday",
- "radroots://%61dd",
- "radroots://today/%61dd",
+ "radroots://user@today",
+ "radroots://user:password@add",
+ "radroots://today:7447",
+ "radroots://today?",
+ "radroots://today?source=widget",
+ "radroots://today#",
+ "radroots://add#composer",
+ "radroots://%74oday",
+ "radroots://%61dd",
+ "radroots://today/%61dd",
] {
if let url = URL(string: rawValue) {
XCTAssertNil(
- RadrootsRootTab.resolve(url: url),
- "Expected decorated or encoded deep link rejection: \(rawValue)"
+ RadrootsRootTab.resolve(url: url),
+ "Expected decorated or encoded deep link rejection: \(rawValue)"
)
}
}
diff --git a/RadrootsTests/RadrootsRuntimeClientTests.swift b/RadrootsTests/RadrootsRuntimeClientTests.swift
@@ -41,10 +41,10 @@ final class RadrootsRuntimeClientTests: XCTestCase {
$0?.generation
}
let secondValues = await [
- secondIterator.next(),
- secondIterator.next(),
- secondIterator.next(),
- secondIterator.next(),
+ secondIterator.next(),
+ secondIterator.next(),
+ secondIterator.next(),
+ secondIterator.next(),
].compactMap { $0?.generation }
XCTAssertEqual(firstValues, [9, 10])
@@ -56,9 +56,9 @@ final class RadrootsRuntimeClientTests: XCTestCase {
func testOverlappingStopsAwaitOneTypedShutdownFailure() async throws {
let failure = RadrootsRuntimeFailure.local(
- operation: "test.shutdown",
- code: "test.shutdown_failed",
- safeMessage: "Shutdown did not complete."
+ operation: "test.shutdown",
+ code: "test.shutdown_failed",
+ safeMessage: "Shutdown did not complete."
)
let harness = RuntimeHarness(shutdownFailure: failure)
let client = RadrootsRuntimeClient(factory: harness.start)
@@ -145,8 +145,8 @@ final class RadrootsRuntimeClientTests: XCTestCase {
func testStartupDeadlineReturnsPromptlyAndClosesLateBackend() async throws {
let harness = RuntimeHarness(startDelayNanoseconds: 60_000_000)
let client = RadrootsRuntimeClient(
- factory: harness.start,
- deadlines: makeDeadlines(startup: 5_000_000)
+ factory: harness.start,
+ deadlines: makeDeadlines(startup: 5_000_000)
)
do {
@@ -217,8 +217,8 @@ final class RadrootsRuntimeClientTests: XCTestCase {
func testOperationDeadlineAndLateValueCannotEscape() async throws {
let harness = RuntimeHarness(snapshotDelayNanoseconds: 60_000_000)
let client = RadrootsRuntimeClient(
- factory: harness.start,
- deadlines: makeDeadlines(operation: 5_000_000)
+ factory: harness.start,
+ deadlines: makeDeadlines(operation: 5_000_000)
)
_ = try await client.start(configuration: makeConfiguration(generation: "09"))
@@ -262,8 +262,8 @@ final class RadrootsRuntimeClientTests: XCTestCase {
func testShutdownDeadlineQuarantinesBackendUntilLateSuccess() async throws {
let harness = RuntimeHarness(shutdownDelayNanoseconds: 60_000_000)
let client = RadrootsRuntimeClient(
- factory: harness.start,
- deadlines: makeDeadlines(shutdown: 5_000_000)
+ factory: harness.start,
+ deadlines: makeDeadlines(shutdown: 5_000_000)
)
_ = try await client.start(configuration: makeConfiguration(generation: "0a"))
@@ -292,9 +292,9 @@ final class RadrootsRuntimeClientTests: XCTestCase {
let client = RadrootsRuntimeClient(factory: harness.start)
let snapshot = try await client.start(configuration: makeConfiguration(generation: "0b"))
let store = RadrootsTodayStore(
- runtimeClient: client,
- contexts: [.defaultContext(snapshot: snapshot)],
- observationDelay: { _ in try await Task.sleep(nanoseconds: 20_000_000) }
+ runtimeClient: client,
+ contexts: [.defaultContext(snapshot: snapshot)],
+ observationDelay: { _ in try await Task.sleep(nanoseconds: 20_000_000) }
)
await store.start()
@@ -314,16 +314,16 @@ final class RadrootsRuntimeClientTests: XCTestCase {
}
private func makeDeadlines(
- startup: UInt64 = 1_000_000_000,
- operation: UInt64 = 1_000_000_000,
- subscription: UInt64 = 1_000_000_000,
- shutdown: UInt64 = 1_000_000_000
+ startup: UInt64 = 1_000_000_000,
+ operation: UInt64 = 1_000_000_000,
+ subscription: UInt64 = 1_000_000_000,
+ shutdown: UInt64 = 1_000_000_000
) -> RadrootsRuntimeDeadlinePolicy {
RadrootsRuntimeDeadlinePolicy(
- startupNanoseconds: startup,
- operationNanoseconds: operation,
- subscriptionNanoseconds: subscription,
- shutdownNanoseconds: shutdown
+ startupNanoseconds: startup,
+ operationNanoseconds: operation,
+ subscriptionNanoseconds: subscription,
+ shutdownNanoseconds: shutdown
)
}
@@ -342,37 +342,37 @@ final class RadrootsRuntimeClientTests: XCTestCase {
private func makeConfiguration(generation: String) -> RadrootsRuntimeLaunchConfiguration {
RadrootsRuntimeLaunchConfiguration(
- applicationSupportDirectory: "/tmp/radroots-runtime-tests-\(generation)",
- publicKeyHex: String(repeating: generation, count: 32),
- sourceGenerationHex: String(repeating: generation, count: 32),
- sourceGenerationCreatedAtUnixMilliseconds: 1,
- protectedData: .available,
- networkProfile: .simulator,
- writableRelays: ["ws://127.0.0.1:7447"],
- blossom: RadrootsBlossomEndpointConfiguration(
- hostKind: .simulator,
- endpointAuthority: .loopbackDevelopment,
- primaryOrigin: "http://127.0.0.1:3000",
- fallbackOrigins: []
- ),
- app: RadrootsRuntimeAppMetadata(
- bundleIdentifier: "org.radroots.tests",
- version: "0.1.0-alpha",
- buildNumber: "1",
- buildSHA: nil
- ),
- signerGeneration: generation,
- signer: TestRuntimeSigner(),
- adoptBootstrapSettings: false
+ applicationSupportDirectory: "/tmp/radroots-runtime-tests-\(generation)",
+ publicKeyHex: String(repeating: generation, count: 32),
+ sourceGenerationHex: String(repeating: generation, count: 32),
+ sourceGenerationCreatedAtUnixMilliseconds: 1,
+ protectedData: .available,
+ networkProfile: .simulator,
+ writableRelays: ["ws://127.0.0.1:7447"],
+ blossom: RadrootsBlossomEndpointConfiguration(
+ hostKind: .simulator,
+ endpointAuthority: .loopbackDevelopment,
+ primaryOrigin: "http://127.0.0.1:3000",
+ fallbackOrigins: []
+ ),
+ app: RadrootsRuntimeAppMetadata(
+ bundleIdentifier: "org.radroots.tests",
+ version: "0.1.0-alpha",
+ buildNumber: "1",
+ buildSHA: nil
+ ),
+ signerGeneration: generation,
+ signer: TestRuntimeSigner(),
+ adoptBootstrapSettings: false
)
}
private func change(generation: UInt64) -> RadrootsRuntimeChange {
RadrootsRuntimeChange(
- schemaVersion: 1,
- generation: generation,
- kind: .today,
- entityID: "card-\(generation)"
+ schemaVersion: 1,
+ generation: generation,
+ kind: .today,
+ entityID: "card-\(generation)"
)
}
}
@@ -400,11 +400,11 @@ private actor RuntimeHarness {
private var receivers: [UUID: @Sendable (RadrootsRuntimeChange) async -> Void] = [:]
init(
- startDelayNanoseconds: UInt64 = 0,
- snapshotDelayNanoseconds: UInt64 = 0,
- shutdownDelayNanoseconds: UInt64 = 10_000_000,
- subscriptionFailures: Int = 0,
- shutdownFailure: RadrootsRuntimeFailure? = nil
+ startDelayNanoseconds: UInt64 = 0,
+ snapshotDelayNanoseconds: UInt64 = 0,
+ shutdownDelayNanoseconds: UInt64 = 10_000_000,
+ subscriptionFailures: Int = 0,
+ shutdownFailure: RadrootsRuntimeFailure? = nil
) {
self.startDelayNanoseconds = startDelayNanoseconds
self.snapshotDelayNanoseconds = snapshotDelayNanoseconds
@@ -423,15 +423,15 @@ private actor RuntimeHarness {
}.value
}
let backend = TestRuntimeBackend(
- harness: self,
- publicKeyHex: configuration.publicKeyHex,
- snapshotDelayNanoseconds: snapshotDelayNanoseconds,
- shutdownDelayNanoseconds: shutdownDelayNanoseconds,
- shutdownFailure: shutdownFailure
+ harness: self,
+ publicKeyHex: configuration.publicKeyHex,
+ snapshotDelayNanoseconds: snapshotDelayNanoseconds,
+ shutdownDelayNanoseconds: shutdownDelayNanoseconds,
+ shutdownFailure: shutdownFailure
)
return await RadrootsRuntimeBackendStart(
- backend: backend,
- snapshot: backend.snapshotValue()
+ backend: backend,
+ snapshot: backend.snapshotValue()
)
}
@@ -442,9 +442,9 @@ private actor RuntimeHarness {
if subscriptionFailures > 0 {
subscriptionFailures -= 1
throw RadrootsRuntimeFailure.local(
- operation: "test.subscribe",
- code: "test.subscribe_retryable",
- safeMessage: "The test subscription is temporarily unavailable."
+ operation: "test.subscribe",
+ code: "test.subscribe_retryable",
+ safeMessage: "The test subscription is temporarily unavailable."
)
}
let id = UUID()
@@ -494,27 +494,27 @@ private actor TestRuntimeBackend: RadrootsRuntimeBackend {
private var closed = false
init(
- harness: RuntimeHarness,
- publicKeyHex: String,
- snapshotDelayNanoseconds: UInt64,
- shutdownDelayNanoseconds: UInt64,
- shutdownFailure: RadrootsRuntimeFailure?
+ harness: RuntimeHarness,
+ publicKeyHex: String,
+ snapshotDelayNanoseconds: UInt64,
+ shutdownDelayNanoseconds: UInt64,
+ shutdownFailure: RadrootsRuntimeFailure?
) {
self.harness = harness
self.snapshotDelayNanoseconds = snapshotDelayNanoseconds
self.shutdownDelayNanoseconds = shutdownDelayNanoseconds
self.shutdownFailure = shutdownFailure
snapshotValueStored = RadrootsRuntimeSnapshot(
- identity: RadrootsRuntimeIdentity(
- publicKeyHex: publicKeyHex,
- hostSignerConfigured: true
- ),
- relay: nil,
- blossomConfiguration: nil,
- blossomEvidence: nil,
- crateName: "radroots_mobile_ffi",
- crateVersion: "0.1.0-alpha",
- isClosed: false
+ identity: RadrootsRuntimeIdentity(
+ publicKeyHex: publicKeyHex,
+ hostSignerConfigured: true
+ ),
+ relay: nil,
+ blossomConfiguration: nil,
+ blossomEvidence: nil,
+ crateName: "radroots_mobile_ffi",
+ crateVersion: "0.1.0-alpha",
+ isClosed: false
)
}
@@ -530,9 +530,9 @@ private actor TestRuntimeBackend: RadrootsRuntimeBackend {
}
if closed {
throw RadrootsRuntimeFailure.local(
- operation: "test.snapshot",
- code: "test.closed",
- safeMessage: "The test runtime is closed."
+ operation: "test.snapshot",
+ code: "test.closed",
+ safeMessage: "The test runtime is closed."
)
}
return snapshotValueStored
@@ -540,27 +540,27 @@ private actor TestRuntimeBackend: RadrootsRuntimeBackend {
func todayPage(request _: RadrootsTodayPageRequest) throws -> RadrootsTodayPage {
throw RadrootsRuntimeFailure.local(
- operation: "test.today.page",
- code: "test.unsupported",
- safeMessage: "Today is not configured for this lifecycle test."
+ operation: "test.today.page",
+ code: "test.unsupported",
+ safeMessage: "Today is not configured for this lifecycle test."
)
}
func refreshToday(
- context _: RadrootsLocalNetwork,
- nowUnixSeconds _: UInt64,
- update _: RadrootsTodayProjectionUpdate
+ context _: RadrootsLocalNetwork,
+ nowUnixSeconds _: UInt64,
+ update _: RadrootsTodayProjectionUpdate
) throws -> RadrootsTodayRefreshReceipt {
throw RadrootsRuntimeFailure.local(
- operation: "test.today.refresh",
- code: "test.unsupported",
- safeMessage: "Today is not configured for this lifecycle test."
+ operation: "test.today.refresh",
+ code: "test.unsupported",
+ safeMessage: "Today is not configured for this lifecycle test."
)
}
func subscribe(
- bufferCapacity _: Int,
- receive: @escaping @Sendable (RadrootsRuntimeChange) async -> Void
+ bufferCapacity _: Int,
+ receive: @escaping @Sendable (RadrootsRuntimeChange) async -> Void
) async throws -> any RadrootsRuntimeSubscriptionToken {
let id = try await harness.addReceiver(receive)
return TestSubscriptionToken(harness: harness, id: id)
diff --git a/RadrootsTests/RadrootsStateMigrationTests.swift b/RadrootsTests/RadrootsStateMigrationTests.swift
@@ -6,64 +6,64 @@ import XCTest
final class RadrootsStateMigrationTests: XCTestCase {
func testRelayValidationMatchesRuntimeProfiles() throws {
XCTAssertEqual(
- try RadrootsNetworkValidator.relays(
- ["wss://radroots.org", "WSS://WRITE.EXAMPLE:443/"],
- profile: .publicNetwork
- ),
- ["wss://write.example"]
+ try RadrootsNetworkValidator.relays(
+ ["wss://radroots.org", "WSS://WRITE.EXAMPLE:443/"],
+ profile: .publicNetwork
+ ),
+ ["wss://write.example"]
)
XCTAssertEqual(
- try RadrootsNetworkValidator.relays(
- ["ws://127.0.0.1:7447"],
- profile: .simulator
- ),
- ["ws://127.0.0.1:7447"]
+ try RadrootsNetworkValidator.relays(
+ ["ws://127.0.0.1:7447"],
+ profile: .simulator
+ ),
+ ["ws://127.0.0.1:7447"]
)
for accepted in [
- "ws://10.0.0.5:7447",
- "ws://172.16.0.5:7447",
- "ws://172.31.255.254:7447",
- "ws://192.168.0.5:7447",
- "ws://[fc00::5]:7447",
- "ws://[fd00::5]:7447",
- "wss://10.0.0.5:7447",
+ "ws://10.0.0.5:7447",
+ "ws://172.16.0.5:7447",
+ "ws://172.31.255.254:7447",
+ "ws://192.168.0.5:7447",
+ "ws://[fc00::5]:7447",
+ "ws://[fd00::5]:7447",
+ "wss://10.0.0.5:7447",
] {
XCTAssertNoThrow(
- try RadrootsNetworkValidator.relays([accepted], profile: .device),
- "Expected device policy to admit \(accepted)"
+ try RadrootsNetworkValidator.relays([accepted], profile: .device),
+ "Expected device policy to admit \(accepted)"
)
}
for denied in [
- "ws://8.8.8.8:7447",
- "ws://device.example:7447",
- "wss://device.example:7447",
- "ws://0.0.0.0:7447",
- "ws://127.0.0.1:7447",
- "ws://169.254.1.1:7447",
- "ws://172.32.0.5:7447",
- "ws://100.64.0.5:7447",
- "ws://224.0.0.1:7447",
- "ws://[::]:7447",
- "ws://[::1]:7447",
- "ws://[fe80::1]:7447",
- "ws://[ff02::1]:7447",
- "ws://[2001:4860:4860::8888]:7447",
+ "ws://8.8.8.8:7447",
+ "ws://device.example:7447",
+ "wss://device.example:7447",
+ "ws://0.0.0.0:7447",
+ "ws://127.0.0.1:7447",
+ "ws://169.254.1.1:7447",
+ "ws://172.32.0.5:7447",
+ "ws://100.64.0.5:7447",
+ "ws://224.0.0.1:7447",
+ "ws://[::]:7447",
+ "ws://[::1]:7447",
+ "ws://[fe80::1]:7447",
+ "ws://[ff02::1]:7447",
+ "ws://[2001:4860:4860::8888]:7447",
] {
XCTAssertThrowsError(
- try RadrootsNetworkValidator.relays([denied], profile: .device),
- "Expected device policy to deny \(denied)"
+ try RadrootsNetworkValidator.relays([denied], profile: .device),
+ "Expected device policy to deny \(denied)"
)
}
for denied in [
- "ws://public.example",
- "wss://localhost",
- "wss://10.0.0.1",
- "wss://user@example.com",
- "wss://relay.example?token=value",
+ "ws://public.example",
+ "wss://localhost",
+ "wss://10.0.0.1",
+ "wss://user@example.com",
+ "wss://relay.example?token=value",
] {
XCTAssertThrowsError(
- try RadrootsNetworkValidator.relays([denied], profile: .publicNetwork),
- "Expected public policy to deny \(denied)"
+ try RadrootsNetworkValidator.relays([denied], profile: .publicNetwork),
+ "Expected public policy to deny \(denied)"
)
}
}
@@ -73,21 +73,21 @@ final class RadrootsStateMigrationTests: XCTestCase {
defer { fixture.remove() }
let fileAccess = RadrootsAppleFileAccess(roots: fixture.roots)
try fileAccess.write(
- .inline(
+ .inline(
Data(
"""
{"format":"radroots_field_ios_relay_settings_v1","relays":["ws://127.0.0.1:7447"]}
""".utf8
)
- ),
- to: RadrootsFileReference(
- scope: .data,
- relativePath: "settings/relay_settings.json"
- )
+ ),
+ to: RadrootsFileReference(
+ scope: .data,
+ relativePath: "settings/relay_settings.json"
+ )
)
let store = RadrootsConfigurationStore(
- bootstrap: fixture.bootstrap,
- roots: fixture.roots
+ bootstrap: fixture.bootstrap,
+ roots: fixture.roots
)
let first = try await store.load()
let second = try await store.load()
@@ -95,11 +95,11 @@ final class RadrootsStateMigrationTests: XCTestCase {
XCTAssertEqual(first.writableRelays, ["ws://127.0.0.1:7447"])
try fileAccess.write(
- .inline(Data("not-json".utf8)),
- to: RadrootsFileReference(
- scope: .data,
- relativePath: "settings/radroots_configuration_v3.json"
- )
+ .inline(Data("not-json".utf8)),
+ to: RadrootsFileReference(
+ scope: .data,
+ relativePath: "settings/radroots_configuration_v3.json"
+ )
)
do {
_ = try await store.load()
@@ -114,17 +114,17 @@ final class RadrootsStateMigrationTests: XCTestCase {
defer { fixture.remove() }
let fileAccess = RadrootsAppleFileAccess(roots: fixture.roots)
try fileAccess.write(
- .inline(
+ .inline(
Data(
"""
{"format":"radroots_ios_configuration_v2","profile":"simulator","writableRelays":["ws://127.0.0.1:7447"],"blossomOrigins":["http://127.0.0.1:3000","http://localhost:3001"]}
""".utf8
)
- ),
- to: RadrootsFileReference(
- scope: .data,
- relativePath: "settings/radroots_configuration_v2.json"
- )
+ ),
+ to: RadrootsFileReference(
+ scope: .data,
+ relativePath: "settings/radroots_configuration_v2.json"
+ )
)
let store = RadrootsConfigurationStore(bootstrap: fixture.bootstrap, roots: fixture.roots)
@@ -133,13 +133,13 @@ final class RadrootsStateMigrationTests: XCTestCase {
XCTAssertEqual(first, second)
XCTAssertEqual(
- first.blossom,
- RadrootsBlossomEndpointConfiguration(
- hostKind: .simulator,
- endpointAuthority: .loopbackDevelopment,
- primaryOrigin: "http://127.0.0.1:3000",
- fallbackOrigins: ["http://localhost:3001"]
- )
+ first.blossom,
+ RadrootsBlossomEndpointConfiguration(
+ hostKind: .simulator,
+ endpointAuthority: .loopbackDevelopment,
+ primaryOrigin: "http://127.0.0.1:3000",
+ fallbackOrigins: ["http://localhost:3001"]
+ )
)
let persisted = try configurationObject(fileAccess)
XCTAssertEqual(persisted["format"] as? String, "radroots_ios_configuration_v3")
@@ -151,12 +151,12 @@ final class RadrootsStateMigrationTests: XCTestCase {
let fingerprint = String(repeating: "a", count: 64)
try await store.confirmCanonicalBlossomConfiguration(
- canonicalBlossomConfiguration(
- primaryOrigin: "http://127.0.0.1:3000",
- fallbackOrigins: ["http://localhost:3001"],
- fingerprint: fingerprint
- ),
- expectedGeneration: first.generation
+ canonicalBlossomConfiguration(
+ primaryOrigin: "http://127.0.0.1:3000",
+ fallbackOrigins: ["http://localhost:3001"],
+ fingerprint: fingerprint
+ ),
+ expectedGeneration: first.generation
)
let confirmed = try configurationObject(fileAccess)
XCTAssertEqual(confirmed["canonicalBlossomConfigFingerprint"] as? String, fingerprint)
@@ -168,25 +168,25 @@ final class RadrootsStateMigrationTests: XCTestCase {
defer { fixture.remove() }
let fileAccess = RadrootsAppleFileAccess(roots: fixture.roots)
_ = try await RadrootsConfigurationStore(
- bootstrap: fixture.bootstrap,
- roots: fixture.roots
+ bootstrap: fixture.bootstrap,
+ roots: fixture.roots
).load()
let originalFingerprint = String(repeating: "b", count: 64)
let originalStore = RadrootsConfigurationStore(
- bootstrap: fixture.bootstrap,
- roots: fixture.roots
+ bootstrap: fixture.bootstrap,
+ roots: fixture.roots
)
try await originalStore.confirmCanonicalBlossomConfiguration(
- canonicalBlossomConfiguration(fingerprint: originalFingerprint),
- expectedGeneration: 1
+ canonicalBlossomConfiguration(fingerprint: originalFingerprint),
+ expectedGeneration: 1
)
let production = RadrootsConfigurationBootstrap(
- runtimeMode: "production",
- relayURLs: ["wss://write.example"],
- blossomOrigins: ["https://blossom.example"],
- keychainServicePrefix: fixture.bootstrap.keychainServicePrefix,
- bundleIdentifier: fixture.bootstrap.bundleIdentifier,
- appMetadata: fixture.bootstrap.appMetadata
+ runtimeMode: "production",
+ relayURLs: ["wss://write.example"],
+ blossomOrigins: ["https://blossom.example"],
+ keychainServicePrefix: fixture.bootstrap.keychainServicePrefix,
+ bundleIdentifier: fixture.bootstrap.bundleIdentifier,
+ appMetadata: fixture.bootstrap.appMetadata
)
let store = RadrootsConfigurationStore(bootstrap: production, roots: fixture.roots)
@@ -198,13 +198,13 @@ final class RadrootsStateMigrationTests: XCTestCase {
XCTAssertEqual(recovered.previousBlossomConfigFingerprint, originalFingerprint)
XCTAssertEqual(recovered.writableRelays, ["wss://write.example"])
XCTAssertEqual(
- recovered.blossom,
- RadrootsBlossomEndpointConfiguration(
- hostKind: .physicalDevice,
- endpointAuthority: .publicWebPKI,
- primaryOrigin: "https://blossom.example",
- fallbackOrigins: []
- )
+ recovered.blossom,
+ RadrootsBlossomEndpointConfiguration(
+ hostKind: .physicalDevice,
+ endpointAuthority: .publicWebPKI,
+ primaryOrigin: "https://blossom.example",
+ fallbackOrigins: []
+ )
)
let repeated = try await store.load()
XCTAssertEqual(repeated, recovered)
@@ -214,8 +214,8 @@ final class RadrootsStateMigrationTests: XCTestCase {
XCTAssertEqual(persisted["activationState"] as? String, "reconfiguration_required")
XCTAssertNil(persisted["canonicalBlossomConfigFingerprint"])
XCTAssertEqual(
- persisted["previousBlossomConfigFingerprint"] as? String,
- originalFingerprint
+ persisted["previousBlossomConfigFingerprint"] as? String,
+ originalFingerprint
)
}
@@ -223,26 +223,26 @@ final class RadrootsStateMigrationTests: XCTestCase {
let fixture = try StateFixture()
defer { fixture.remove() }
let original = RadrootsConfigurationStore(
- bootstrap: fixture.bootstrap,
- roots: fixture.roots
+ bootstrap: fixture.bootstrap,
+ roots: fixture.roots
)
let first = try await original.load()
let originalFingerprint = String(repeating: "c", count: 64)
try await original.confirmCanonicalBlossomConfiguration(
- canonicalBlossomConfiguration(fingerprint: originalFingerprint),
- expectedGeneration: first.generation
+ canonicalBlossomConfiguration(fingerprint: originalFingerprint),
+ expectedGeneration: first.generation
)
let changedBootstrap = RadrootsConfigurationBootstrap(
- runtimeMode: fixture.bootstrap.runtimeMode,
- relayURLs: ["ws://127.0.0.1:7448"],
- blossomOrigins: ["http://127.0.0.1:3001"],
- keychainServicePrefix: fixture.bootstrap.keychainServicePrefix,
- bundleIdentifier: fixture.bootstrap.bundleIdentifier,
- appMetadata: fixture.bootstrap.appMetadata
+ runtimeMode: fixture.bootstrap.runtimeMode,
+ relayURLs: ["ws://127.0.0.1:7448"],
+ blossomOrigins: ["http://127.0.0.1:3001"],
+ keychainServicePrefix: fixture.bootstrap.keychainServicePrefix,
+ bundleIdentifier: fixture.bootstrap.bundleIdentifier,
+ appMetadata: fixture.bootstrap.appMetadata
)
let changed = RadrootsConfigurationStore(
- bootstrap: changedBootstrap,
- roots: fixture.roots
+ bootstrap: changedBootstrap,
+ roots: fixture.roots
)
let pending = try await changed.load()
@@ -255,11 +255,11 @@ final class RadrootsStateMigrationTests: XCTestCase {
let replacementFingerprint = String(repeating: "d", count: 64)
try await changed.confirmCanonicalBlossomConfiguration(
- canonicalBlossomConfiguration(
- primaryOrigin: "http://127.0.0.1:3001",
- fingerprint: replacementFingerprint
- ),
- expectedGeneration: pending.generation
+ canonicalBlossomConfiguration(
+ primaryOrigin: "http://127.0.0.1:3001",
+ fingerprint: replacementFingerprint
+ ),
+ expectedGeneration: pending.generation
)
let active = try await changed.load()
XCTAssertEqual(active.activationState, .current)
@@ -269,8 +269,8 @@ final class RadrootsStateMigrationTests: XCTestCase {
RadrootsAppleFileAccess(roots: fixture.roots)
)
XCTAssertEqual(
- persisted["canonicalBlossomConfigFingerprint"] as? String,
- replacementFingerprint
+ persisted["canonicalBlossomConfigFingerprint"] as? String,
+ replacementFingerprint
)
}
@@ -278,25 +278,25 @@ final class RadrootsStateMigrationTests: XCTestCase {
let fixture = try StateFixture()
defer { fixture.remove() }
let original = RadrootsConfigurationStore(
- bootstrap: fixture.bootstrap,
- roots: fixture.roots
+ bootstrap: fixture.bootstrap,
+ roots: fixture.roots
)
let first = try await original.load()
try await original.confirmCanonicalBlossomConfiguration(
- canonicalBlossomConfiguration(fingerprint: String(repeating: "f", count: 64)),
- expectedGeneration: first.generation
+ canonicalBlossomConfiguration(fingerprint: String(repeating: "f", count: 64)),
+ expectedGeneration: first.generation
)
let changedBootstrap = RadrootsConfigurationBootstrap(
- runtimeMode: fixture.bootstrap.runtimeMode,
- relayURLs: ["ws://127.0.0.1:7448"],
- blossomOrigins: ["http://127.0.0.1:3001"],
- keychainServicePrefix: fixture.bootstrap.keychainServicePrefix,
- bundleIdentifier: fixture.bootstrap.bundleIdentifier,
- appMetadata: fixture.bootstrap.appMetadata
+ runtimeMode: fixture.bootstrap.runtimeMode,
+ relayURLs: ["ws://127.0.0.1:7448"],
+ blossomOrigins: ["http://127.0.0.1:3001"],
+ keychainServicePrefix: fixture.bootstrap.keychainServicePrefix,
+ bundleIdentifier: fixture.bootstrap.bundleIdentifier,
+ appMetadata: fixture.bootstrap.appMetadata
)
let changed = RadrootsConfigurationStore(
- bootstrap: changedBootstrap,
- roots: fixture.roots
+ bootstrap: changedBootstrap,
+ roots: fixture.roots
)
let pending = try await changed.load()
@@ -319,26 +319,26 @@ final class RadrootsStateMigrationTests: XCTestCase {
let fixture = try StateFixture()
defer { fixture.remove() }
let bootstrap = RadrootsConfigurationBootstrap(
- runtimeMode: "production",
- relayURLs: [],
- blossomOrigins: ["https://media.example"],
- keychainServicePrefix: fixture.bootstrap.keychainServicePrefix,
- bundleIdentifier: fixture.bootstrap.bundleIdentifier,
- appMetadata: fixture.bootstrap.appMetadata
+ runtimeMode: "production",
+ relayURLs: [],
+ blossomOrigins: ["https://media.example"],
+ keychainServicePrefix: fixture.bootstrap.keychainServicePrefix,
+ bundleIdentifier: fixture.bootstrap.bundleIdentifier,
+ appMetadata: fixture.bootstrap.appMetadata
)
let store = RadrootsConfigurationStore(bootstrap: bootstrap, roots: fixture.roots)
let selected = try await store.load()
try await store.confirmCanonicalBlossomConfiguration(
- RadrootsBlossomConfigurationStatus(
- schemaVersion: 1,
- hostKind: "physical_device",
- endpointAuthority: "public_webpki",
- primaryOrigin: "https://media.example",
- fallbackOrigins: [],
- configFingerprint: String(repeating: "e", count: 64)
- ),
- expectedGeneration: selected.generation
+ RadrootsBlossomConfigurationStatus(
+ schemaVersion: 1,
+ hostKind: "physical_device",
+ endpointAuthority: "public_webpki",
+ primaryOrigin: "https://media.example",
+ fallbackOrigins: [],
+ configFingerprint: String(repeating: "e", count: 64)
+ ),
+ expectedGeneration: selected.generation
)
let persisted = try configurationObject(
@@ -353,30 +353,30 @@ final class RadrootsStateMigrationTests: XCTestCase {
defer { fixture.remove() }
let fileAccess = RadrootsAppleFileAccess(roots: fixture.roots)
try fileAccess.write(
- .inline(
+ .inline(
Data(
"""
{"format":"radroots_ios_configuration_v3","profile":"simulator","writableRelays":["ws://127.0.0.1:7447"],"blossom":{"hostKind":"physical_device","endpointAuthority":"public_web_pki","primaryOrigin":"https://wrong.example","fallbackOrigins":[]}}
""".utf8
)
- ),
- to: RadrootsFileReference(
- scope: .data,
- relativePath: "settings/radroots_configuration_v3.json"
- )
+ ),
+ to: RadrootsFileReference(
+ scope: .data,
+ relativePath: "settings/radroots_configuration_v3.json"
+ )
)
let store = RadrootsConfigurationStore(bootstrap: fixture.bootstrap, roots: fixture.roots)
let recovered = try await store.load()
XCTAssertEqual(
- recovered.blossom,
- RadrootsBlossomEndpointConfiguration(
- hostKind: .simulator,
- endpointAuthority: .loopbackDevelopment,
- primaryOrigin: "http://127.0.0.1:3000",
- fallbackOrigins: []
- )
+ recovered.blossom,
+ RadrootsBlossomEndpointConfiguration(
+ hostKind: .simulator,
+ endpointAuthority: .loopbackDevelopment,
+ primaryOrigin: "http://127.0.0.1:3000",
+ fallbackOrigins: []
+ )
)
}
@@ -384,11 +384,11 @@ final class RadrootsStateMigrationTests: XCTestCase {
_ fileAccess: RadrootsAppleFileAccess
) throws -> [String: Any] {
let source = try fileAccess.read(
- RadrootsFileReference(
- scope: .data,
- relativePath: "settings/radroots_configuration_v3.json"
- ),
- mode: .inline
+ RadrootsFileReference(
+ scope: .data,
+ relativePath: "settings/radroots_configuration_v3.json"
+ ),
+ mode: .inline
)
guard case let .inline(data) = source,
let object = try JSONSerialization.jsonObject(with: data) as? [String: Any]
@@ -399,17 +399,17 @@ final class RadrootsStateMigrationTests: XCTestCase {
}
private func canonicalBlossomConfiguration(
- primaryOrigin: String = "http://127.0.0.1:3000",
- fallbackOrigins: [String] = [],
- fingerprint: String
+ primaryOrigin: String = "http://127.0.0.1:3000",
+ fallbackOrigins: [String] = [],
+ fingerprint: String
) -> RadrootsBlossomConfigurationStatus {
RadrootsBlossomConfigurationStatus(
- schemaVersion: 1,
- hostKind: "simulator",
- endpointAuthority: "loopback_development",
- primaryOrigin: primaryOrigin,
- fallbackOrigins: fallbackOrigins,
- configFingerprint: fingerprint
+ schemaVersion: 1,
+ hostKind: "simulator",
+ endpointAuthority: "loopback_development",
+ primaryOrigin: primaryOrigin,
+ fallbackOrigins: fallbackOrigins,
+ configFingerprint: fingerprint
)
}
@@ -417,25 +417,25 @@ final class RadrootsStateMigrationTests: XCTestCase {
let fixture = try StateFixture()
defer { fixture.remove() }
let firstStore = RadrootsConfigurationStore(
- bootstrap: fixture.bootstrap,
- roots: fixture.roots
+ bootstrap: fixture.bootstrap,
+ roots: fixture.roots
)
let first = try await firstStore.sourceGeneration()
let secondStore = RadrootsConfigurationStore(
- bootstrap: fixture.bootstrap,
- roots: fixture.roots
+ bootstrap: fixture.bootstrap,
+ roots: fixture.roots
)
let second = try await secondStore.sourceGeneration()
XCTAssertEqual(first, second)
let key = String(repeating: "ab", count: 32)
XCTAssertEqual(
- RadrootsStableVisualIdentity(publicKeyHex: key),
- RadrootsStableVisualIdentity(publicKeyHex: key)
+ RadrootsStableVisualIdentity(publicKeyHex: key),
+ RadrootsStableVisualIdentity(publicKeyHex: key)
)
XCTAssertNotEqual(
- RadrootsStableVisualIdentity(publicKeyHex: key).digestHex,
- RadrootsStableVisualIdentity(publicKeyHex: String(repeating: "cd", count: 32)).digestHex
+ RadrootsStableVisualIdentity(publicKeyHex: key).digestHex,
+ RadrootsStableVisualIdentity(publicKeyHex: String(repeating: "cd", count: 32)).digestHex
)
}
@@ -448,28 +448,28 @@ final class RadrootsStateMigrationTests: XCTestCase {
UserDefaults(suiteName: servicePrefix)?.removePersistentDomain(forName: servicePrefix)
}
let legacyKey = RadrootsSecureStoreKey(
- namespace: "nostr_identity",
- name: "selected_secret_hex"
+ namespace: "nostr_identity",
+ name: "selected_secret_hex"
)
try secureStore.put(
- Data(String(repeating: "01", count: 32).utf8),
- for: legacyKey,
- policy: .secureLocalSecret
+ Data(String(repeating: "01", count: 32).utf8),
+ for: legacyKey,
+ policy: .secureLocalSecret
)
let custody = try RadrootsIdentityCustody(
- configuration: RadrootsIdentityCustodyConfiguration(
- namespace: "radroots_identity_v1",
- secretPolicy: .secureLocalSecret
- ),
- secureStore: secureStore,
- metadataStore: metadataStore,
- userPresence: AllowingUserPresence()
+ configuration: RadrootsIdentityCustodyConfiguration(
+ namespace: "radroots_identity_v1",
+ secretPolicy: .secureLocalSecret
+ ),
+ secureStore: secureStore,
+ metadataStore: metadataStore,
+ userPresence: AllowingUserPresence()
)
let store = RadrootsIdentityStore(
- custody: custody,
- secureStore: secureStore,
- servicePrefix: servicePrefix,
- userDefaults: defaults
+ custody: custody,
+ secureStore: secureStore,
+ servicePrefix: servicePrefix,
+ userDefaults: defaults
)
let first = try await store.loadAndMigrate()
XCTAssertEqual(first.state, .recoveryRequired)
@@ -489,23 +489,23 @@ final class RadrootsStateMigrationTests: XCTestCase {
UserDefaults(suiteName: servicePrefix)?.removePersistentDomain(forName: servicePrefix)
}
defaults.set(
- Data("not-json".utf8),
- forKey: "field_ios.identity.public_metadata.\(servicePrefix)"
+ Data("not-json".utf8),
+ forKey: "field_ios.identity.public_metadata.\(servicePrefix)"
)
let custody = try RadrootsIdentityCustody(
- configuration: RadrootsIdentityCustodyConfiguration(
- namespace: "radroots_identity_v1",
- secretPolicy: .secureLocalSecret
- ),
- secureStore: secureStore,
- metadataStore: InMemoryIdentityMetadataStore(),
- userPresence: AllowingUserPresence()
+ configuration: RadrootsIdentityCustodyConfiguration(
+ namespace: "radroots_identity_v1",
+ secretPolicy: .secureLocalSecret
+ ),
+ secureStore: secureStore,
+ metadataStore: InMemoryIdentityMetadataStore(),
+ userPresence: AllowingUserPresence()
)
let store = RadrootsIdentityStore(
- custody: custody,
- secureStore: secureStore,
- servicePrefix: servicePrefix,
- userDefaults: defaults
+ custody: custody,
+ secureStore: secureStore,
+ servicePrefix: servicePrefix,
+ userDefaults: defaults
)
do {
_ = try await store.loadAndMigrate()
@@ -518,32 +518,32 @@ final class RadrootsStateMigrationTests: XCTestCase {
func testRuntimeSignerUsesCanonicalOperationIDInsteadOfSignerRequestDigest() async throws {
let secureStore = InMemorySecureStore()
let custody = try RadrootsIdentityCustody(
- configuration: RadrootsIdentityCustodyConfiguration(
- namespace: "radroots_identity_v1",
- secretPolicy: .secureLocalSecret
- ),
- secureStore: secureStore,
- metadataStore: InMemoryIdentityMetadataStore(),
- userPresence: AllowingUserPresence()
+ configuration: RadrootsIdentityCustodyConfiguration(
+ namespace: "radroots_identity_v1",
+ secretPolicy: .secureLocalSecret
+ ),
+ secureStore: secureStore,
+ metadataStore: InMemoryIdentityMetadataStore(),
+ userPresence: AllowingUserPresence()
)
let servicePrefix = "org.radroots.tests.signer.\(UUID().uuidString.lowercased())"
let store = RadrootsIdentityStore(
- custody: custody,
- secureStore: secureStore,
- servicePrefix: servicePrefix
+ custody: custody,
+ secureStore: secureStore,
+ servicePrefix: servicePrefix
)
let identity = try await store.create()
let signer = try await store.signer(for: identity)
let operationID = UUID().uuidString.lowercased()
- let outcome = await signer.sign(
+ let outcome = try await signer.sign(
RadrootsRuntimeSigningRequest(
- operationID: operationID,
- signerRequestID: String(repeating: "ab", count: 32),
- publicKeyHex: try XCTUnwrap(identity.publicKeyHex),
- purpose: .blossomUpload,
- deadlineUnixMilliseconds: UInt64(Date().timeIntervalSince1970 * 1000) + 60_000,
- digest: Data(repeating: 0xcd, count: 32)
+ operationID: operationID,
+ signerRequestID: String(repeating: "ab", count: 32),
+ publicKeyHex: XCTUnwrap(identity.publicKeyHex),
+ purpose: .blossomUpload,
+ deadlineUnixMilliseconds: UInt64(Date().timeIntervalSince1970 * 1000) + 60000,
+ digest: Data(repeating: 0xCD, count: 32)
)
)
@@ -564,23 +564,23 @@ private struct StateFixture {
.appendingPathComponent("radroots-state-tests-\(UUID().uuidString.lowercased())")
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: false)
roots = try RadrootsAppleFileRoots(
- appIdentifier: "org.radroots.tests",
- dataRoot: root.appendingPathComponent("data"),
- cacheRoot: root.appendingPathComponent("cache"),
- temporaryRoot: root.appendingPathComponent("tmp")
+ appIdentifier: "org.radroots.tests",
+ dataRoot: root.appendingPathComponent("data"),
+ cacheRoot: root.appendingPathComponent("cache"),
+ temporaryRoot: root.appendingPathComponent("tmp")
)
bootstrap = RadrootsConfigurationBootstrap(
- runtimeMode: "localhost-dev",
- relayURLs: ["ws://127.0.0.1:8080"],
- blossomOrigins: ["http://127.0.0.1:3000"],
- keychainServicePrefix: "org.radroots.tests",
+ runtimeMode: "localhost-dev",
+ relayURLs: ["ws://127.0.0.1:8080"],
+ blossomOrigins: ["http://127.0.0.1:3000"],
+ keychainServicePrefix: "org.radroots.tests",
+ bundleIdentifier: "org.radroots.tests",
+ appMetadata: RadrootsRuntimeAppMetadata(
bundleIdentifier: "org.radroots.tests",
- appMetadata: RadrootsRuntimeAppMetadata(
- bundleIdentifier: "org.radroots.tests",
- version: "0.1.0-alpha",
- buildNumber: "1",
- buildSHA: nil
- )
+ version: "0.1.0-alpha",
+ buildNumber: "1",
+ buildSHA: nil
+ )
)
}
@@ -594,9 +594,9 @@ private final class InMemorySecureStore: RadrootsSecureStore, @unchecked Sendabl
private var values: [RadrootsSecureStoreKey: Data] = [:]
func put(
- _ value: Data,
- for key: RadrootsSecureStoreKey,
- policy _: RadrootsSecretAccessPolicy
+ _ value: Data,
+ for key: RadrootsSecureStoreKey,
+ policy _: RadrootsSecretAccessPolicy
) throws {
lock.withLock { values[key] = value }
}
@@ -638,10 +638,10 @@ private final class InMemoryIdentityMetadataStore: RadrootsIdentityMetadataStore
private struct AllowingUserPresence: RadrootsUserPresence {
func currentStatus() async throws -> RadrootsUserPresenceStatus {
RadrootsUserPresenceStatus(
- support: .deviceCredential,
- biometryKind: .none,
- canEvaluateDeviceCredential: true,
- canEvaluateBiometrics: false
+ support: .deviceCredential,
+ biometryKind: .none,
+ canEvaluateDeviceCredential: true,
+ canEvaluateBiometrics: false
)
}
diff --git a/RadrootsTests/RadrootsSupportingStoreTests.swift b/RadrootsTests/RadrootsSupportingStoreTests.swift
@@ -81,8 +81,8 @@ final class RadrootsSupportingStoreTests: XCTestCase {
XCTAssertEqual(store.settings?.identity.identities.count, 1)
store.networkEnvironment = .physicalDevice
store.relays = [
- RadrootsRelayPreference(url: "wss://radroots.org/", access: .readWrite),
- RadrootsRelayPreference(url: "wss://read.example/", access: .readOnly),
+ RadrootsRelayPreference(url: "wss://radroots.org/", access: .readWrite),
+ RadrootsRelayPreference(url: "wss://read.example/", access: .readOnly),
]
store.blossomPrimaryOrigin = "https://blossom.radroots.org"
store.allowCellularUploads = false
@@ -147,40 +147,40 @@ final class RadrootsSupportingStoreTests: XCTestCase {
private static func configuration() -> RadrootsRuntimeLaunchConfiguration {
RadrootsRuntimeLaunchConfiguration(
- applicationSupportDirectory: "/tmp/radroots-supporting-tests",
- publicKeyHex: String(repeating: "a", count: 64),
- sourceGenerationHex: String(repeating: "c", count: 64),
- sourceGenerationCreatedAtUnixMilliseconds: 1,
- protectedData: .available,
- networkProfile: .simulator,
- writableRelays: ["ws://127.0.0.1:7447"],
- blossom: RadrootsBlossomEndpointConfiguration(
- hostKind: .simulator,
- endpointAuthority: .loopbackDevelopment,
- primaryOrigin: "http://127.0.0.1:3000",
- fallbackOrigins: []
- ),
- app: RadrootsRuntimeAppMetadata(
- bundleIdentifier: "org.radroots.supporting-tests",
- version: "0.1.0-alpha",
- buildNumber: "1",
- buildSHA: nil
- ),
- signerGeneration: "supporting-tests",
- signer: SupportingSigner(),
- adoptBootstrapSettings: false
+ applicationSupportDirectory: "/tmp/radroots-supporting-tests",
+ publicKeyHex: String(repeating: "a", count: 64),
+ sourceGenerationHex: String(repeating: "c", count: 64),
+ sourceGenerationCreatedAtUnixMilliseconds: 1,
+ protectedData: .available,
+ networkProfile: .simulator,
+ writableRelays: ["ws://127.0.0.1:7447"],
+ blossom: RadrootsBlossomEndpointConfiguration(
+ hostKind: .simulator,
+ endpointAuthority: .loopbackDevelopment,
+ primaryOrigin: "http://127.0.0.1:3000",
+ fallbackOrigins: []
+ ),
+ app: RadrootsRuntimeAppMetadata(
+ bundleIdentifier: "org.radroots.supporting-tests",
+ version: "0.1.0-alpha",
+ buildNumber: "1",
+ buildSHA: nil
+ ),
+ signerGeneration: "supporting-tests",
+ signer: SupportingSigner(),
+ adoptBootstrapSettings: false
)
}
private static func context(id: String) -> RadrootsLocalNetwork {
RadrootsLocalNetwork(
- schemaVersion: 1,
- id: id,
- label: "Farm",
- relayURLs: ["ws://127.0.0.1:7447"],
- locality: "Metchosin",
- followedAuthors: [],
- generation: 1
+ schemaVersion: 1,
+ id: id,
+ label: "Farm",
+ relayURLs: ["ws://127.0.0.1:7447"],
+ locality: "Metchosin",
+ followedAuthors: [],
+ generation: 1
)
}
}
@@ -213,16 +213,16 @@ private actor SupportingBackend: RadrootsRuntimeBackend {
func snapshotValue() -> RadrootsRuntimeSnapshot {
RadrootsRuntimeSnapshot(
- identity: RadrootsRuntimeIdentity(
- publicKeyHex: String(repeating: "a", count: 64),
- hostSignerConfigured: true
- ),
- relay: nil,
- blossomConfiguration: nil,
- blossomEvidence: nil,
- crateName: "radroots_mobile_ffi",
- crateVersion: "0.1.0-alpha",
- isClosed: closed
+ identity: RadrootsRuntimeIdentity(
+ publicKeyHex: String(repeating: "a", count: 64),
+ hostSignerConfigured: true
+ ),
+ relay: nil,
+ blossomConfiguration: nil,
+ blossomEvidence: nil,
+ crateName: "radroots_mobile_ffi",
+ crateVersion: "0.1.0-alpha",
+ isClosed: closed
)
}
@@ -235,18 +235,18 @@ private actor SupportingBackend: RadrootsRuntimeBackend {
}
func refreshToday(
- context _: RadrootsLocalNetwork,
- nowUnixSeconds _: UInt64,
- update _: RadrootsTodayProjectionUpdate
+ context _: RadrootsLocalNetwork,
+ nowUnixSeconds _: UInt64,
+ update _: RadrootsTodayProjectionUpdate
) throws -> RadrootsTodayRefreshReceipt {
throw unsupported()
}
func search(
- context: RadrootsLocalNetwork,
- query: String,
- limit _: UInt16,
- asOfUnixSeconds _: UInt64
+ context: RadrootsLocalNetwork,
+ query: String,
+ limit _: UInt16,
+ asOfUnixSeconds _: UInt64
) async throws -> [RadrootsSearchResult] {
request = SearchRequest(contextID: context.id, query: query)
if searchDelayNanoseconds > 0 {
@@ -255,20 +255,20 @@ private actor SupportingBackend: RadrootsRuntimeBackend {
let card = Self.card()
let profile = Self.profile()
return [
- RadrootsSearchResult(type: .card, id: "card", card: card, profile: nil),
- RadrootsSearchResult(type: .card, id: "card", card: card, profile: nil),
- RadrootsSearchResult(type: .profile, id: "profile", card: nil, profile: profile),
+ RadrootsSearchResult(type: .card, id: "card", card: card, profile: nil),
+ RadrootsSearchResult(type: .card, id: "card", card: card, profile: nil),
+ RadrootsSearchResult(type: .profile, id: "profile", card: nil, profile: profile),
]
}
func me(
- context _: RadrootsLocalNetwork,
- asOfUnixSeconds _: UInt64
+ context _: RadrootsLocalNetwork,
+ asOfUnixSeconds _: UInt64
) -> RadrootsMeSnapshot {
RadrootsMeSnapshot(
- publicKey: String(repeating: "a", count: 64),
- profile: Self.profile(),
- cards: [Self.card()]
+ publicKey: String(repeating: "a", count: 64),
+ profile: Self.profile(),
+ cards: [Self.card()]
)
}
@@ -286,24 +286,24 @@ private actor SupportingBackend: RadrootsRuntimeBackend {
throw failure(code: "invalid_relay_endpoint")
}
settings = RadrootsMobileSettings(
- revision: settings.revision + 1,
- identity: settings.identity,
- networkEnvironment: input.networkEnvironment,
- relays: input.relays,
- blossomAuthority: input.blossomAuthority,
- blossomPrimaryOrigin: input.blossomPrimaryOrigin,
- blossomFallbackOrigins: input.blossomFallbackOrigins,
- allowCellularDownloads: input.allowCellularDownloads,
- allowCellularUploads: input.allowCellularUploads,
- allowBackgroundTransfers: input.allowBackgroundTransfers,
- mediaCacheBytes: input.mediaCacheBytes,
- mediaCacheArtifacts: input.mediaCacheArtifacts
+ revision: settings.revision + 1,
+ identity: settings.identity,
+ networkEnvironment: input.networkEnvironment,
+ relays: input.relays,
+ blossomAuthority: input.blossomAuthority,
+ blossomPrimaryOrigin: input.blossomPrimaryOrigin,
+ blossomFallbackOrigins: input.blossomFallbackOrigins,
+ allowCellularDownloads: input.allowCellularDownloads,
+ allowCellularUploads: input.allowCellularUploads,
+ allowBackgroundTransfers: input.allowBackgroundTransfers,
+ mediaCacheBytes: input.mediaCacheBytes,
+ mediaCacheArtifacts: input.mediaCacheArtifacts
)
return RadrootsSettingsTransition(
- settings: settings,
- runtimeRestartRequired: true,
- outboxRequeueRequired: true,
- mediaCacheInvalidationRequired: true
+ settings: settings,
+ runtimeRestartRequired: true,
+ outboxRequeueRequired: true,
+ mediaCacheInvalidationRequired: true
)
}
@@ -314,31 +314,31 @@ private actor SupportingBackend: RadrootsRuntimeBackend {
func advanceProfile(operationID _: String) -> RadrootsProfileStatus {
profileStatus(
- state: .complete,
- revision: 2,
- settlement: RadrootsOperationSettlement(
- artifacts: 0,
- signed: 1,
- admitted: 1,
- pending: 0,
- retryable: 0,
- indeterminate: 0,
- failedTerminal: 0,
- cancelled: 0,
- deliveryPlans: 1,
- deliverySatisfied: 1,
- deliveryPending: 0,
- deliveryRetryable: 0,
- deliveryExhausted: 0,
- deliveryFailedTerminal: 0,
- deliveryCancelled: 0
- )
+ state: .complete,
+ revision: 2,
+ settlement: RadrootsOperationSettlement(
+ artifacts: 0,
+ signed: 1,
+ admitted: 1,
+ pending: 0,
+ retryable: 0,
+ indeterminate: 0,
+ failedTerminal: 0,
+ cancelled: 0,
+ deliveryPlans: 1,
+ deliverySatisfied: 1,
+ deliveryPending: 0,
+ deliveryRetryable: 0,
+ deliveryExhausted: 0,
+ deliveryFailedTerminal: 0,
+ deliveryCancelled: 0
+ )
)
}
func subscribe(
- bufferCapacity _: Int,
- receive _: @escaping @Sendable (RadrootsRuntimeChange) async -> Void
+ bufferCapacity _: Int,
+ receive _: @escaping @Sendable (RadrootsRuntimeChange) async -> Void
) -> any RadrootsRuntimeSubscriptionToken {
SupportingSubscriptionToken()
}
@@ -359,116 +359,116 @@ private actor SupportingBackend: RadrootsRuntimeBackend {
private func failure(code: String) -> RadrootsRuntimeFailure {
RadrootsRuntimeFailure(
- schemaVersion: 1,
- code: code,
- category: "invalid_argument",
- retryable: false,
- recoveryActions: [],
- operationID: "test.settings",
- capabilityID: nil,
- safeMessage: "The settings value is invalid."
+ schemaVersion: 1,
+ code: code,
+ category: "invalid_argument",
+ retryable: false,
+ recoveryActions: [],
+ operationID: "test.settings",
+ capabilityID: nil,
+ safeMessage: "The settings value is invalid."
)
}
private func profileStatus(
- state: RadrootsOutboxState,
- revision: UInt64,
- settlement: RadrootsOperationSettlement?
+ state: RadrootsOutboxState,
+ revision: UInt64,
+ settlement: RadrootsOperationSettlement?
) -> RadrootsProfileStatus {
RadrootsProfileStatus(
- id: "profile-operation",
- revision: revision,
- authorPublicKey: String(repeating: "a", count: 64),
- state: state,
- deliveryID: state == .complete ? "delivery" : nil,
- createdAtUnixMilliseconds: 1,
- updatedAtUnixMilliseconds: revision,
- settlement: settlement
+ id: "profile-operation",
+ revision: revision,
+ authorPublicKey: String(repeating: "a", count: 64),
+ state: state,
+ deliveryID: state == .complete ? "delivery" : nil,
+ createdAtUnixMilliseconds: 1,
+ updatedAtUnixMilliseconds: revision,
+ settlement: settlement
)
}
private static func initialSettings() -> RadrootsMobileSettings {
RadrootsMobileSettings(
- revision: 1,
- identity: RadrootsSettingsIdentityState(
- identities: [
- RadrootsSettingsIdentity(
- id: "identity",
- publicKeyHex: String(repeating: "a", count: 64)
- ),
- ],
- activeIdentityID: "identity",
- lockState: .unlocked,
- pendingImportOperationID: nil
- ),
- networkEnvironment: .simulator,
- relays: [
- RadrootsRelayPreference(url: "ws://127.0.0.1:7447", access: .readWrite),
+ revision: 1,
+ identity: RadrootsSettingsIdentityState(
+ identities: [
+ RadrootsSettingsIdentity(
+ id: "identity",
+ publicKeyHex: String(repeating: "a", count: 64)
+ ),
],
- blossomAuthority: .loopbackDevelopment,
- blossomPrimaryOrigin: "http://127.0.0.1:3000",
- blossomFallbackOrigins: [],
- allowCellularDownloads: true,
- allowCellularUploads: true,
- allowBackgroundTransfers: true,
- mediaCacheBytes: 256 * 1_048_576,
- mediaCacheArtifacts: 1024
+ activeIdentityID: "identity",
+ lockState: .unlocked,
+ pendingImportOperationID: nil
+ ),
+ networkEnvironment: .simulator,
+ relays: [
+ RadrootsRelayPreference(url: "ws://127.0.0.1:7447", access: .readWrite),
+ ],
+ blossomAuthority: .loopbackDevelopment,
+ blossomPrimaryOrigin: "http://127.0.0.1:3000",
+ blossomFallbackOrigins: [],
+ allowCellularDownloads: true,
+ allowCellularUploads: true,
+ allowBackgroundTransfers: true,
+ mediaCacheBytes: 256 * 1_048_576,
+ mediaCacheArtifacts: 1024
)
}
private func unsupported() -> RadrootsRuntimeFailure {
.local(
- operation: "test.supporting",
- code: "test.unsupported",
- safeMessage: "Unsupported test operation."
+ operation: "test.supporting",
+ code: "test.unsupported",
+ safeMessage: "Unsupported test operation."
)
}
private static func profile() -> RadrootsProfileSummary {
RadrootsProfileSummary(
- authorPublicKey: String(repeating: "a", count: 64),
- name: "moss",
- displayName: "Moss Farm",
- about: "Local roots",
- picture: nil,
- banner: nil,
- nip05: "moss@example.com",
- website: "https://moss.example",
- lightningAddress: "moss@example.com"
+ authorPublicKey: String(repeating: "a", count: 64),
+ name: "moss",
+ displayName: "Moss Farm",
+ about: "Local roots",
+ picture: nil,
+ banner: nil,
+ nip05: "moss@example.com",
+ website: "https://moss.example",
+ lightningAddress: "moss@example.com"
)
}
private static func card() -> RadrootsTodayCard {
RadrootsTodayCard(
- id: "card",
- type: .foodAvailability,
- sourceEventID: String(repeating: "e", count: 64),
- sourceAddress: nil,
- authorPublicKey: String(repeating: "a", count: 64),
- contractID: "radroots.food_availability.v1",
- title: "Carrots",
- content: "Freshly picked",
- authoredAtUnixSeconds: 1_800_000_000,
- effectiveAtUnixSeconds: 1_800_000_000,
- eventStartUnixSeconds: nil,
- eventEndUnixSeconds: nil,
- location: "Metchosin",
- priceAmount: "3",
- priceCurrency: "CAD",
- priceUnit: "lb",
- quantity: "12",
- foodSummary: "Fresh carrots",
- foodPublishedAtUnixSeconds: 1_800_000_000,
- foodStatus: "active",
- contextRank: 1,
- inclusionReason: "local",
- media: [],
- lifecycle: .active,
- rankDigest: nil,
- authorProfile: profile(),
- thread: [],
- localOperationID: nil,
- localOperationState: nil
+ id: "card",
+ type: .foodAvailability,
+ sourceEventID: String(repeating: "e", count: 64),
+ sourceAddress: nil,
+ authorPublicKey: String(repeating: "a", count: 64),
+ contractID: "radroots.food_availability.v1",
+ title: "Carrots",
+ content: "Freshly picked",
+ authoredAtUnixSeconds: 1_800_000_000,
+ effectiveAtUnixSeconds: 1_800_000_000,
+ eventStartUnixSeconds: nil,
+ eventEndUnixSeconds: nil,
+ location: "Metchosin",
+ priceAmount: "3",
+ priceCurrency: "CAD",
+ priceUnit: "lb",
+ quantity: "12",
+ foodSummary: "Fresh carrots",
+ foodPublishedAtUnixSeconds: 1_800_000_000,
+ foodStatus: "active",
+ contextRank: 1,
+ inclusionReason: "local",
+ media: [],
+ lifecycle: .active,
+ rankDigest: nil,
+ authorProfile: profile(),
+ thread: [],
+ localOperationID: nil,
+ localOperationState: nil
)
}
}
diff --git a/RadrootsTests/RadrootsTodayStoreTests.swift b/RadrootsTests/RadrootsTodayStoreTests.swift
@@ -9,24 +9,24 @@ final class RadrootsTodayStoreTests: XCTestCase {
let second = makeCard(id: "b", type: .ask, authoredAt: 1_800_000_000)
let backend = TodayBackend(
pages: [
- "one:first": RadrootsTodayPage(
- asOfUnixSeconds: 1_800_000_100,
- items: [first],
- nextCursor: "same-time"
- ),
- "one:same-time": RadrootsTodayPage(
- asOfUnixSeconds: 1_800_000_100,
- items: [first, second],
- nextCursor: nil
- ),
+ "one:first": RadrootsTodayPage(
+ asOfUnixSeconds: 1_800_000_100,
+ items: [first],
+ nextCursor: "same-time"
+ ),
+ "one:same-time": RadrootsTodayPage(
+ asOfUnixSeconds: 1_800_000_100,
+ items: [first, second],
+ nextCursor: nil
+ ),
]
)
let client = try await startedClient(backend: backend)
let store = RadrootsTodayStore(
- runtimeClient: client,
- contexts: [context],
- pageSize: 1,
- now: { 1_800_000_100 }
+ runtimeClient: client,
+ contexts: [context],
+ pageSize: 1,
+ now: { 1_800_000_100 }
)
await store.reload(refreshProjection: false)
@@ -47,25 +47,25 @@ final class RadrootsTodayStoreTests: XCTestCase {
let stale = makeCard(id: "stale", type: .update)
let current = makeCard(id: "current", type: .photoUpdate)
let backend = TodayBackend(
- pages: [
- "slow:first": RadrootsTodayPage(
- asOfUnixSeconds: 1_800_000_100,
- items: [stale],
- nextCursor: nil
- ),
- "current:first": RadrootsTodayPage(
- asOfUnixSeconds: 1_800_000_100,
- items: [current],
- nextCursor: nil
- ),
- ],
- delays: ["slow": 80_000_000]
+ pages: [
+ "slow:first": RadrootsTodayPage(
+ asOfUnixSeconds: 1_800_000_100,
+ items: [stale],
+ nextCursor: nil
+ ),
+ "current:first": RadrootsTodayPage(
+ asOfUnixSeconds: 1_800_000_100,
+ items: [current],
+ nextCursor: nil
+ ),
+ ],
+ delays: ["slow": 80_000_000]
)
let client = try await startedClient(backend: backend)
let store = RadrootsTodayStore(
- runtimeClient: client,
- contexts: [firstContext, secondContext],
- now: { 1_800_000_100 }
+ runtimeClient: client,
+ contexts: [firstContext, secondContext],
+ now: { 1_800_000_100 }
)
let staleRequest = Task { await store.reload(refreshProjection: false) }
@@ -82,45 +82,45 @@ final class RadrootsTodayStoreTests: XCTestCase {
@MainActor
func testFiveCardsExposeProfileThreadMediaOverlayAndAccessibleProductFields() {
let profile = RadrootsProfileSummary(
- authorPublicKey: String(repeating: "a", count: 64),
- name: "fieldnotes",
- displayName: "Field Notes Farm",
- about: nil,
- picture: nil,
- banner: nil,
- nip05: "farm@example.com",
- website: nil,
- lightningAddress: nil
+ authorPublicKey: String(repeating: "a", count: 64),
+ name: "fieldnotes",
+ displayName: "Field Notes Farm",
+ about: nil,
+ picture: nil,
+ banner: nil,
+ nip05: "farm@example.com",
+ website: nil,
+ lightningAddress: nil
)
let failedMedia = RadrootsMediaReference(
- referenceFingerprint: String(repeating: "d", count: 64),
- url: "https://blossom.example/\(String(repeating: "b", count: 64))",
- sha256: String(repeating: "b", count: 64),
- mediaType: "image/jpeg",
- width: 1200,
- height: 800,
- byteSize: 400,
- alt: "Carrots in a basket",
- verification: .failed
+ referenceFingerprint: String(repeating: "d", count: 64),
+ url: "https://blossom.example/\(String(repeating: "b", count: 64))",
+ sha256: String(repeating: "b", count: 64),
+ mediaType: "image/jpeg",
+ width: 1200,
+ height: 800,
+ byteSize: 400,
+ alt: "Carrots in a basket",
+ verification: .failed
)
let thread = RadrootsThreadEntry(
- id: "reply",
- authorPublicKey: String(repeating: "c", count: 64),
- content: "Are these available Saturday?",
- authoredAtUnixSeconds: 1_800_000_001,
- type: .reply,
- root: "root",
- parentEventID: "root",
- authorProfile: nil
+ id: "reply",
+ authorPublicKey: String(repeating: "c", count: 64),
+ content: "Are these available Saturday?",
+ authoredAtUnixSeconds: 1_800_000_001,
+ type: .reply,
+ root: "root",
+ parentEventID: "root",
+ authorProfile: nil
)
let cards = RadrootsTodayCardType.allCases.map { type in
makeCard(
- id: type.rawValue,
- type: type,
- profile: profile,
- media: type == .photoUpdate ? [failedMedia] : [],
- thread: type == .ask ? [thread] : [],
- localState: type == .foodAvailability ? "partially_delivered" : nil
+ id: type.rawValue,
+ type: type,
+ profile: profile,
+ media: type == .photoUpdate ? [failedMedia] : [],
+ thread: type == .ask ? [thread] : [],
+ localState: type == .foodAvailability ? "partially_delivered" : nil
)
}
@@ -139,35 +139,35 @@ final class RadrootsTodayStoreTests: XCTestCase {
let context = makeContext(id: "offline", label: "Offline")
let cached = makeCard(id: "cached", type: .update)
let failure = RadrootsRuntimeFailure(
- schemaVersion: 1,
- code: "today_relay_offline",
- category: "relay",
- retryable: true,
- recoveryActions: ["retry"],
- operationID: "test.today.refresh",
- capabilityID: "nostr_source",
- safeMessage: "Saved posts are available offline."
+ schemaVersion: 1,
+ code: "today_relay_offline",
+ category: "relay",
+ retryable: true,
+ recoveryActions: ["retry"],
+ operationID: "test.today.refresh",
+ capabilityID: "nostr_source",
+ safeMessage: "Saved posts are available offline."
)
let backend = TodayBackend(
- pages: [
- "offline:first": RadrootsTodayPage(
- asOfUnixSeconds: 1_800_000_100,
- items: [cached],
- nextCursor: "cached-next"
- ),
- "offline:cached-next": RadrootsTodayPage(
- asOfUnixSeconds: 1_800_000_100,
- items: [makeCard(id: "cached-next", type: .ask)],
- nextCursor: nil
- ),
- ],
- refreshFailure: failure
+ pages: [
+ "offline:first": RadrootsTodayPage(
+ asOfUnixSeconds: 1_800_000_100,
+ items: [cached],
+ nextCursor: "cached-next"
+ ),
+ "offline:cached-next": RadrootsTodayPage(
+ asOfUnixSeconds: 1_800_000_100,
+ items: [makeCard(id: "cached-next", type: .ask)],
+ nextCursor: nil
+ ),
+ ],
+ refreshFailure: failure
)
let client = try await startedClient(backend: backend)
let store = RadrootsTodayStore(
- runtimeClient: client,
- contexts: [context],
- now: { 1_800_000_100 }
+ runtimeClient: client,
+ contexts: [context],
+ now: { 1_800_000_100 }
)
await store.reload()
@@ -191,83 +191,83 @@ final class RadrootsTodayStoreTests: XCTestCase {
private func makeConfiguration() -> RadrootsRuntimeLaunchConfiguration {
RadrootsRuntimeLaunchConfiguration(
- applicationSupportDirectory: "/tmp/radroots-today-tests",
- publicKeyHex: String(repeating: "ab", count: 32),
- sourceGenerationHex: String(repeating: "cd", count: 32),
- sourceGenerationCreatedAtUnixMilliseconds: 1,
- protectedData: .available,
- networkProfile: .simulator,
- writableRelays: ["ws://127.0.0.1:7447"],
- blossom: RadrootsBlossomEndpointConfiguration(
- hostKind: .simulator,
- endpointAuthority: .loopbackDevelopment,
- primaryOrigin: "http://127.0.0.1:3000",
- fallbackOrigins: []
- ),
- app: RadrootsRuntimeAppMetadata(
- bundleIdentifier: "org.radroots.today-tests",
- version: "0.1.0-alpha",
- buildNumber: "1",
- buildSHA: nil
- ),
- signerGeneration: "today-tests",
- signer: TodayTestSigner(),
- adoptBootstrapSettings: false
+ applicationSupportDirectory: "/tmp/radroots-today-tests",
+ publicKeyHex: String(repeating: "ab", count: 32),
+ sourceGenerationHex: String(repeating: "cd", count: 32),
+ sourceGenerationCreatedAtUnixMilliseconds: 1,
+ protectedData: .available,
+ networkProfile: .simulator,
+ writableRelays: ["ws://127.0.0.1:7447"],
+ blossom: RadrootsBlossomEndpointConfiguration(
+ hostKind: .simulator,
+ endpointAuthority: .loopbackDevelopment,
+ primaryOrigin: "http://127.0.0.1:3000",
+ fallbackOrigins: []
+ ),
+ app: RadrootsRuntimeAppMetadata(
+ bundleIdentifier: "org.radroots.today-tests",
+ version: "0.1.0-alpha",
+ buildNumber: "1",
+ buildSHA: nil
+ ),
+ signerGeneration: "today-tests",
+ signer: TodayTestSigner(),
+ adoptBootstrapSettings: false
)
}
}
private func makeContext(id: String, label: String) -> RadrootsLocalNetwork {
RadrootsLocalNetwork(
- schemaVersion: 1,
- id: id,
- label: label,
- relayURLs: ["ws://127.0.0.1:7447"],
- locality: nil,
- followedAuthors: [],
- generation: 1
+ schemaVersion: 1,
+ id: id,
+ label: label,
+ relayURLs: ["ws://127.0.0.1:7447"],
+ locality: nil,
+ followedAuthors: [],
+ generation: 1
)
}
private func makeCard(
- id: String,
- type: RadrootsTodayCardType,
- authoredAt: UInt64 = 1_800_000_000,
- profile: RadrootsProfileSummary? = nil,
- media: [RadrootsMediaReference] = [],
- thread: [RadrootsThreadEntry] = [],
- localState: String? = nil
+ id: String,
+ type: RadrootsTodayCardType,
+ authoredAt: UInt64 = 1_800_000_000,
+ profile: RadrootsProfileSummary? = nil,
+ media: [RadrootsMediaReference] = [],
+ thread: [RadrootsThreadEntry] = [],
+ localState: String? = nil
) -> RadrootsTodayCard {
RadrootsTodayCard(
- id: id,
- type: type,
- sourceEventID: "event-\(id)",
- sourceAddress: type == .event || type == .foodAvailability ? "address-\(id)" : nil,
- authorPublicKey: String(repeating: "a", count: 64),
- contractID: "contract-\(type.rawValue)",
- title: type == .event ? "Saturday market" : type == .foodAvailability ? "Carrots" : nil,
- content: type == .ask ? "Who has seedlings?" : "Fresh from the field",
- authoredAtUnixSeconds: authoredAt,
- effectiveAtUnixSeconds: authoredAt,
- eventStartUnixSeconds: type == .event ? authoredAt + 3600 : nil,
- eventEndUnixSeconds: nil,
- location: type == .event || type == .foodAvailability ? "Town square" : nil,
- priceAmount: type == .foodAvailability ? "3" : nil,
- priceCurrency: type == .foodAvailability ? "CAD" : nil,
- priceUnit: type == .foodAvailability ? "lb" : nil,
- quantity: type == .foodAvailability ? "12" : nil,
- foodSummary: type == .foodAvailability ? "Fresh carrots" : nil,
- foodPublishedAtUnixSeconds: type == .foodAvailability ? authoredAt : nil,
- foodStatus: type == .foodAvailability ? "active" : nil,
- contextRank: 1,
- inclusionReason: "locality_missing_fallback",
- media: media,
- lifecycle: .active,
- rankDigest: "rank-\(id)",
- authorProfile: profile,
- thread: thread,
- localOperationID: localState == nil ? nil : "operation-\(id)",
- localOperationState: localState
+ id: id,
+ type: type,
+ sourceEventID: "event-\(id)",
+ sourceAddress: type == .event || type == .foodAvailability ? "address-\(id)" : nil,
+ authorPublicKey: String(repeating: "a", count: 64),
+ contractID: "contract-\(type.rawValue)",
+ title: type == .event ? "Saturday market" : type == .foodAvailability ? "Carrots" : nil,
+ content: type == .ask ? "Who has seedlings?" : "Fresh from the field",
+ authoredAtUnixSeconds: authoredAt,
+ effectiveAtUnixSeconds: authoredAt,
+ eventStartUnixSeconds: type == .event ? authoredAt + 3600 : nil,
+ eventEndUnixSeconds: nil,
+ location: type == .event || type == .foodAvailability ? "Town square" : nil,
+ priceAmount: type == .foodAvailability ? "3" : nil,
+ priceCurrency: type == .foodAvailability ? "CAD" : nil,
+ priceUnit: type == .foodAvailability ? "lb" : nil,
+ quantity: type == .foodAvailability ? "12" : nil,
+ foodSummary: type == .foodAvailability ? "Fresh carrots" : nil,
+ foodPublishedAtUnixSeconds: type == .foodAvailability ? authoredAt : nil,
+ foodStatus: type == .foodAvailability ? "active" : nil,
+ contextRank: 1,
+ inclusionReason: "locality_missing_fallback",
+ media: media,
+ lifecycle: .active,
+ rankDigest: "rank-\(id)",
+ authorProfile: profile,
+ thread: thread,
+ localOperationID: localState == nil ? nil : "operation-\(id)",
+ localOperationState: localState
)
}
@@ -288,9 +288,9 @@ private actor TodayBackend: RadrootsRuntimeBackend {
private var closed = false
init(
- pages: [String: RadrootsTodayPage],
- delays: [String: UInt64] = [:],
- refreshFailure: RadrootsRuntimeFailure? = nil
+ pages: [String: RadrootsTodayPage],
+ delays: [String: UInt64] = [:],
+ refreshFailure: RadrootsRuntimeFailure? = nil
) {
self.pages = pages
self.delays = delays
@@ -299,16 +299,16 @@ private actor TodayBackend: RadrootsRuntimeBackend {
func snapshot() -> RadrootsRuntimeSnapshot {
RadrootsRuntimeSnapshot(
- identity: RadrootsRuntimeIdentity(
- publicKeyHex: String(repeating: "ab", count: 32),
- hostSignerConfigured: true
- ),
- relay: nil,
- blossomConfiguration: nil,
- blossomEvidence: nil,
- crateName: "radroots_mobile_ffi",
- crateVersion: "0.1.0-alpha",
- isClosed: closed
+ identity: RadrootsRuntimeIdentity(
+ publicKeyHex: String(repeating: "ab", count: 32),
+ hostSignerConfigured: true
+ ),
+ relay: nil,
+ blossomConfiguration: nil,
+ blossomEvidence: nil,
+ crateName: "radroots_mobile_ffi",
+ crateVersion: "0.1.0-alpha",
+ isClosed: closed
)
}
@@ -319,36 +319,36 @@ private actor TodayBackend: RadrootsRuntimeBackend {
let key = "\(request.context.id):\(request.cursor ?? "first")"
guard let page = pages[key] else {
throw RadrootsRuntimeFailure.local(
- operation: "test.today.page",
- code: "test.page_missing",
- safeMessage: "The requested test page is missing."
+ operation: "test.today.page",
+ code: "test.page_missing",
+ safeMessage: "The requested test page is missing."
)
}
return page
}
func refreshToday(
- context _: RadrootsLocalNetwork,
- nowUnixSeconds _: UInt64,
- update: RadrootsTodayProjectionUpdate
+ context _: RadrootsLocalNetwork,
+ nowUnixSeconds _: UInt64,
+ update: RadrootsTodayProjectionUpdate
) throws -> RadrootsTodayRefreshReceipt {
if let refreshFailure {
throw refreshFailure
}
return RadrootsTodayRefreshReceipt(
- update: update,
- sourceEvents: 0,
- visibleCards: 0,
- profiles: 0,
- threadEntries: 0,
- contentGeneration: 1,
- changed: false
+ update: update,
+ sourceEvents: 0,
+ visibleCards: 0,
+ profiles: 0,
+ threadEntries: 0,
+ contentGeneration: 1,
+ changed: false
)
}
func subscribe(
- bufferCapacity _: Int,
- receive _: @escaping @Sendable (RadrootsRuntimeChange) async -> Void
+ bufferCapacity _: Int,
+ receive _: @escaping @Sendable (RadrootsRuntimeChange) async -> Void
) -> any RadrootsRuntimeSubscriptionToken {
TodaySubscriptionToken()
}
diff --git a/RadrootsUITests/RadrootsRemoteQualificationUITests.swift b/RadrootsUITests/RadrootsRemoteQualificationUITests.swift
@@ -34,7 +34,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
app.launch()
let failureCode = app.descendants(matching: .any)["radroots.runtime.failure_code"]
- for _ in 0..<8 where !failureCode.exists {
+ for _ in 0 ..< 8 where !failureCode.exists {
for identifier in [
"radroots.identity.create",
"radroots.identity.unlock",
@@ -69,13 +69,34 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
guard preparePhotoUpdate(app, marker: marker) != nil else { return }
let save = app.buttons["radroots.add.save"]
scrollTo(app, element: save)
- XCTAssertTrue(save.waitForExistence(timeout: 10))
- XCTAssertTrue(waitUntilHittable(save, timeout: 10))
+ guard save.waitForExistence(timeout: 10),
+ waitUntilHittable(save, timeout: 10),
+ isUnobscured(save, above: app.tabBars.firstMatch)
+ else {
+ return XCTFail("The prepared Photo update could not be saved through the visible UI")
+ }
+ let addRoot = app.descendants(matching: .any)["radroots.add.root"]
save.tap()
let saved = app.staticTexts.matching(identifier: "radroots.add.status").firstMatch
- XCTAssertTrue(
- waitForLabel(saved, label: "Draft saved on this device.", timeout: 20)
- )
+ let saveStarted = NSPredicate { _, _ in
+ addRoot.value as? String == "Working"
+ || saved.exists && saved.label == "Draft saved on this device."
+ }
+ let startExpectation = XCTNSPredicateExpectation(predicate: saveStarted, object: app)
+ guard XCTWaiter.wait(for: [startExpectation], timeout: 10) == .completed else {
+ return XCTFail("The visible Save draft action did not start durable persistence")
+ }
+ let saveSettled = NSPredicate { _, _ in
+ addRoot.value as? String == "Ready"
+ }
+ let settleExpectation = XCTNSPredicateExpectation(predicate: saveSettled, object: app)
+ guard XCTWaiter.wait(for: [settleExpectation], timeout: 60) == .completed else {
+ return XCTFail("The visible Save draft action did not finish durable persistence")
+ }
+ scrollAddFormToTop(app)
+ guard waitForLabel(saved, label: "Draft saved on this device.", timeout: 10) else {
+ return XCTFail("The visible Add status did not confirm the durable draft save")
+ }
XCTAssertTrue(assertUnverifiedDraft(app))
app.terminate()
@@ -115,10 +136,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
guard openAdd(app) != nil, openDrafts(app) else {
return XCTFail("The durable outbox was unavailable after the final relaunch")
}
- XCTAssertGreaterThanOrEqual(
- app.buttons.matching(NSPredicate(format: "label == 'View'")).count,
- 5
- )
+ assertDraftOutboxContainsAtLeast(app, count: expected.count)
app.buttons["Done"].tap()
}
@@ -375,7 +393,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
XCUIDevice.shared.press(.home)
Thread.sleep(forTimeInterval: 60)
- for _ in 0..<24 {
+ for _ in 0 ..< 24 {
app.activate()
if app.tabBars.firstMatch.waitForExistence(timeout: 5) {
return
@@ -443,7 +461,8 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
if issue.auditType == .contrast,
let element = issue.element,
element.identifier.isEmpty,
- element.label.isEmpty {
+ element.label.isEmpty
+ {
return true
}
if issue.auditType == .contrast,
@@ -474,12 +493,16 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
let navigationBar = app.navigationBars.firstMatch
if navigationBar.exists {
let boundary = navigationBar.frame.maxY
- if frame.minY < boundary && frame.maxY > boundary { return true }
+ if frame.minY < boundary, frame.maxY > boundary {
+ return true
+ }
}
let tabBar = app.tabBars.firstMatch
if tabBar.exists {
let boundary = tabBar.frame.minY
- if frame.minY < boundary && frame.maxY > boundary { return true }
+ if frame.minY < boundary, frame.maxY > boundary {
+ return true
+ }
}
return false
}
@@ -529,38 +552,38 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
case "radroots.add.content":
switch type {
case "Update":
- return "What’s happening locally?"
+ "What’s happening locally?"
case "Photo update":
- return "What should neighbors know?"
+ "What should neighbors know?"
case "Ask":
- return "What do you need or want to know?"
+ "What do you need or want to know?"
case "Event":
- return "Event details (optional)"
+ "Event details (optional)"
case "Food availability":
- return "Details"
+ "Details"
default:
preconditionFailure("Unrecognized Add type: \(type)")
}
case "radroots.add.title":
- return type == "Food availability" ? "Food" : "Title"
+ type == "Food availability" ? "Food" : "Title"
case "radroots.add.summary":
- return "Short summary"
+ "Short summary"
case "radroots.add.location":
- return type == "Food availability" ? "Location" : "Location (optional)"
+ type == "Food availability" ? "Location" : "Location (optional)"
case "radroots.add.event_timing":
- return "When, Specific time"
+ "When, Specific time"
case "radroots.add.event.start":
- return "Starts"
+ "Starts"
case "radroots.add.event.end":
- return "Ends"
+ "Ends"
case "radroots.add.price":
- return "Price"
+ "Price"
case "radroots.add.currency":
- return "Currency"
+ "Currency"
case "radroots.add.unit":
- return "Unit, Choose a unit"
+ "Unit, Choose a unit"
case "radroots.add.quantity":
- return "Quantity available (optional)"
+ "Quantity available (optional)"
default:
preconditionFailure("Unrecognized Add field identifier: \(identifier)")
}
@@ -610,7 +633,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
@MainActor
private func reachRoot(_ app: XCUIApplication) {
- for _ in 0..<6 {
+ for _ in 0 ..< 6 {
if app.tabBars.firstMatch.waitForExistence(timeout: 3) {
return
}
@@ -644,7 +667,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
}
let meList = meSheet.descendants(matching: .collectionView).firstMatch
let settings = app.descendants(matching: .any)["radroots.support.settings"]
- for _ in 0..<8 where !settings.exists {
+ for _ in 0 ..< 8 where !settings.exists {
meList.swipeUp()
}
guard settings.waitForExistence(timeout: 20) else {
@@ -689,7 +712,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
account.tap()
let settings = app.descendants(matching: .any)["radroots.support.settings"]
- for _ in 0..<5 where !settings.exists {
+ for _ in 0 ..< 5 where !settings.exists {
app.swipeUp()
}
guard settings.waitForExistence(timeout: 20) else { return "settings=unavailable" }
@@ -704,7 +727,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
let serverErrorCode = app.descendants(matching: .any)[
"radroots.settings.blossom.server_error_code"
]
- for _ in 0..<6 where !httpStatus.exists || !errorCode.exists || !serverErrorCode.exists {
+ for _ in 0 ..< 6 where !httpStatus.exists || !errorCode.exists || !serverErrorCode.exists {
app.swipeUp()
}
_ = httpStatus.waitForExistence(timeout: 10)
@@ -720,7 +743,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
let add = app.tabBars.buttons["Add"]
guard add.waitForExistence(timeout: 10) else { return nil }
let type = app.descendants(matching: .any)["radroots.add.type"]
- for _ in 0..<3 {
+ for _ in 0 ..< 3 {
add.tap()
if type.waitForExistence(timeout: 10) {
return type
@@ -739,7 +762,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
XCTAssertTrue(newDraft.waitForExistence(timeout: 10))
newDraft.tap()
- for _ in 0..<3 where !app.buttons["Photo update"].exists {
+ for _ in 0 ..< 3 where !app.buttons["Photo update"].exists {
type.tap()
_ = app.buttons["Photo update"].waitForExistence(timeout: 10)
}
@@ -755,7 +778,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
let content = app.descendants(matching: .any)["radroots.add.content"]
var selectedPhotoUpdate = false
- for _ in 0..<3 where !selectedPhotoUpdate {
+ for _ in 0 ..< 3 where !selectedPhotoUpdate {
photoUpdate.tap()
selectedPhotoUpdate = waitForValue(type, value: "Photo update", timeout: 10)
if !selectedPhotoUpdate, !photoUpdate.exists {
@@ -780,11 +803,25 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
XCTAssertFalse(app.keyboards.firstMatch.waitForExistence(timeout: 5))
let library = app.descendants(matching: .any)["radroots.add.media.library"]
- XCTAssertTrue(library.waitForExistence(timeout: 10))
- XCTAssertTrue(library.isEnabled)
+ scrollTo(app, element: library)
+ guard library.waitForExistence(timeout: 10), waitUntilHittable(library, timeout: 10) else {
+ XCTFail("The Photo Library action did not become available through the visible UI")
+ return nil
+ }
library.tap()
+ let preparedStatus = app.staticTexts.matching(
+ NSPredicate(format: "label == 'Photo prepared. Add descriptive text before publishing.'")
+ ).firstMatch
+ guard preparedStatus.waitForExistence(timeout: 60) else {
+ XCTFail("The selected Photo update did not report the visible prepared state")
+ return nil
+ }
let prepared = app.descendants(matching: .any)["radroots.add.media.prepared"]
- XCTAssertTrue(prepared.waitForExistence(timeout: 30))
+ scrollTo(app, element: prepared)
+ guard prepared.waitForExistence(timeout: 10) else {
+ XCTFail("The selected Photo update did not reach the visible prepared state")
+ return nil
+ }
return readySubmit(app)
}
@@ -1056,7 +1093,12 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
@MainActor
private func scrollTo(_ app: XCUIApplication, element: XCUIElement) {
let root = app.descendants(matching: .any)["radroots.add.root"]
- for attempt in 0..<16 where !element.exists || !element.isHittable {
+ let tabBar = app.tabBars.firstMatch
+ for attempt in 0 ..< 16 where !isVisibleInAddViewport(
+ element,
+ root: root,
+ above: tabBar
+ ) {
if element.exists {
let targetFrame = element.frame
let rootFrame = root.frame
@@ -1074,9 +1116,27 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
}
@MainActor
+ private func isVisibleInAddViewport(
+ _ element: XCUIElement,
+ root: XCUIElement,
+ above obstruction: XCUIElement
+ ) -> Bool {
+ guard element.exists, element.isHittable, root.exists, obstruction.exists else {
+ return false
+ }
+ let frame = element.frame
+ let rootFrame = root.frame
+ return frame.height > 0
+ && frame.minY >= rootFrame.minY
+ && frame.maxY <= min(rootFrame.maxY, obstruction.frame.minY)
+ }
+
+ @MainActor
private func scrollAddFormToTop(_ app: XCUIApplication) {
let root = app.descendants(matching: .any)["radroots.add.root"]
- for _ in 0..<8 { root.swipeDown() }
+ for _ in 0 ..< 8 {
+ root.swipeDown()
+ }
}
@MainActor
@@ -1094,11 +1154,13 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
throw QualificationError.missingProductSurface
}
for marker in markers.reversed() {
- for _ in 0..<8 { feed.swipeDown() }
+ for _ in 0 ..< 8 {
+ feed.swipeDown()
+ }
let card = app.descendants(matching: .any).matching(
NSPredicate(format: "label CONTAINS %@", marker)
).firstMatch
- for _ in 0..<6 where !card.exists {
+ for _ in 0 ..< 6 where !card.exists {
feed.swipeUp()
}
guard card.waitForExistence(timeout: 30) else {
@@ -1112,7 +1174,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
private func readySubmit(_ app: XCUIApplication) -> XCUIElement? {
let submit = app.descendants(matching: .any)["radroots.add.submit"]
let addRoot = app.descendants(matching: .any)["radroots.add.root"]
- for _ in 0..<8 {
+ for _ in 0 ..< 8 {
if isUnobscured(submit, above: app.tabBars.firstMatch) {
break
}
@@ -1196,6 +1258,39 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
}
@MainActor
+ private func assertDraftOutboxContainsAtLeast(_ app: XCUIApplication, count: Int) {
+ let sheet = app.descendants(matching: .any)["radroots.add.drafts.sheet"]
+ let list = sheet.descendants(matching: .collectionView).firstMatch
+ guard list.waitForExistence(timeout: 10) else {
+ return XCTFail("The visible Drafts list was unavailable")
+ }
+
+ for _ in 0 ..< 8 {
+ list.swipeDown()
+ }
+ var identifiers = Set<String>()
+ let rows = app.descendants(matching: .any).matching(
+ NSPredicate(format: "identifier BEGINSWITH 'radroots.add.draft.'")
+ )
+ for _ in 0 ..< 12 where identifiers.count < count {
+ for index in 0 ..< rows.count {
+ let identifier = rows.element(boundBy: index).identifier
+ if !identifier.contains(".media_status.") {
+ identifiers.insert(identifier)
+ }
+ }
+ if identifiers.count < count {
+ list.swipeUp()
+ }
+ }
+ XCTAssertGreaterThanOrEqual(
+ identifiers.count,
+ count,
+ "The visible Drafts list omitted persisted outbox rows; identifiers=\(identifiers.sorted())"
+ )
+ }
+
+ @MainActor
private func waitForWorkToFinish(
_ app: XCUIApplication,
submit: XCUIElement,
@@ -1258,7 +1353,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
let drafts = app.buttons["radroots.add.drafts"]
guard drafts.waitForExistence(timeout: 10) else { return false }
let sheet = app.descendants(matching: .any)["radroots.add.drafts.sheet"]
- for _ in 0..<3 {
+ for _ in 0 ..< 3 {
drafts.tap()
if sheet.waitForExistence(timeout: 10) {
return true
@@ -1277,7 +1372,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
@MainActor
private func focusForTextInput(_ element: XCUIElement, timeout: TimeInterval) -> Bool {
let focused = NSPredicate(format: "hasKeyboardFocus == true")
- for _ in 0..<3 {
+ for _ in 0 ..< 3 {
element.tap()
let expectation = XCTNSPredicateExpectation(predicate: focused, object: element)
if XCTWaiter.wait(for: [expectation], timeout: timeout / 3) == .completed {
diff --git a/RadrootsUITests/RadrootsRootShellUITests.swift b/RadrootsUITests/RadrootsRootShellUITests.swift
@@ -39,13 +39,13 @@ final class RadrootsRootShellUITests: XCTestCase {
tabBar.buttons["Today"].tap()
XCTAssertTrue(today.waitForExistence(timeout: 5))
- app.open(try XCTUnwrap(URL(string: "radroots://add")))
+ try app.open(XCTUnwrap(URL(string: "radroots://add")))
XCTAssertTrue(add.waitForExistence(timeout: 2))
- app.open(try XCTUnwrap(URL(string: "radroots://today")))
+ try app.open(XCTUnwrap(URL(string: "radroots://today")))
XCTAssertTrue(today.waitForExistence(timeout: 2))
- app.open(try XCTUnwrap(URL(string: "radroots://add/extra")))
+ try app.open(XCTUnwrap(URL(string: "radroots://add/extra")))
XCTAssertTrue(today.waitForExistence(timeout: 2))
XCTAssertFalse(add.exists)
}
diff --git a/scripts/linux-shared-rust.sh b/scripts/linux-shared-rust.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if [[ -z "${EXT_BUILD_RUN_ACTIVE:-}" || -z "${EXT_BUILD_PROJECT_DIR:-}" ]]; then
+ echo "linux-shared-rust: run through cargo extbuild run --" >&2
+ exit 1
+fi
+
+readonly SOURCE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+readonly RUNNER_IMAGE="docker.io/library/rust:1.97.1-slim-trixie@sha256:fc0648ac2962539be80bd424729a20fd80f7b64bfba7e90bbd642aed6c697c5a"
+readonly RUNNER_ROOT="$EXT_BUILD_PROJECT_DIR/linux-x86_64"
+readonly RUNNER_CARGO_HOME="$RUNNER_ROOT/cargo-home"
+readonly RUNNER_TARGET="$RUNNER_ROOT/target"
+
+command -v docker >/dev/null || {
+ echo "linux-shared-rust: docker is unavailable" >&2
+ exit 1
+}
+mkdir -p "$RUNNER_CARGO_HOME" "$RUNNER_TARGET"
+
+docker run --rm --init --platform linux/amd64 \
+ --user "$(id -u):$(id -g)" \
+ --volume "$SOURCE_ROOT:/workspace:ro" \
+ --volume "$RUNNER_CARGO_HOME:/cargo-home" \
+ --volume "$RUNNER_TARGET:/target" \
+ --workdir /workspace \
+ --env CARGO_HOME=/cargo-home \
+ --env CARGO_TARGET_DIR=/target \
+ "$RUNNER_IMAGE" \
+ /bin/bash -c '
+ set -euo pipefail
+ [[ "$(uname -s)" == "Linux" ]]
+ [[ "$(uname -m)" == "x86_64" ]]
+ cargo check --workspace --all-targets --locked
+ cargo test --workspace --all-targets --locked
+ '
diff --git a/scripts/swift-quality.sh b/scripts/swift-quality.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT"
+
+readonly -a SOURCE_PATHS=(
+ Package.swift
+ Radroots
+ RadrootsPublicAPITests
+ RadrootsTests
+ RadrootsUITests
+)
+
+command -v swiftformat >/dev/null || {
+ echo "swift-quality: swiftformat is unavailable" >&2
+ exit 1
+}
+command -v swiftlint >/dev/null || {
+ echo "swift-quality: swiftlint is unavailable" >&2
+ exit 1
+}
+
+swiftformat --lint --strict --config .swiftformat "${SOURCE_PATHS[@]}"
+swiftlint lint \
+ --strict \
+ --quiet \
+ --no-cache \
+ --silence-deprecation-warnings \
+ --config .swiftlint.yml \
+ "${SOURCE_PATHS[@]}"
diff --git a/scripts/verify-package-contract.sh b/scripts/verify-package-contract.sh
@@ -87,6 +87,13 @@ grep -Fq 'RadrootsUITests/RadrootsRemoteQualificationUITests/testLocalSocialAcce
"$repo_root/scripts/xcode.sh"
grep -Fq 'RadrootsUITests/RadrootsRemoteQualificationUITests/testLocalSocialDeterministicPersonas' \
"$repo_root/scripts/xcode.sh"
+grep -Fq 'requested_content_size=large' "$repo_root/scripts/xcode.sh"
+grep -Fq 'requested_content_size=accessibility-extra-extra-extra-large' \
+ "$repo_root/scripts/xcode.sh"
+grep -Fq 'xcrun simctl ui "$simulator_id" content_size "$requested_content_size"' \
+ "$repo_root/scripts/xcode.sh"
+grep -Fq 'xcrun simctl ui "$simulator_id" content_size "$previous_content_size"' \
+ "$repo_root/scripts/xcode.sh"
grep -Fq 'verify-accessibility' "$repo_root/scripts/local-social-fixture.py"
grep -Fq 'verify-persona-fixture' "$repo_root/scripts/local-social-fixture.py"
grep -Fq 'verify-persona' "$repo_root/scripts/local-social-fixture.py"
@@ -126,6 +133,14 @@ grep -Fq '#if DEBUG' "$repo_root/Radroots/App/RadrootsRemoteQualification.swift"
grep -Fq 'case "simulator": runtimeMode = "simulator"' \
"$repo_root/Radroots/App/RadrootsRemoteQualification.swift"
test -f "$repo_root/scripts/local-social-fixture.py"
+test -f "$repo_root/.swiftformat"
+test -f "$repo_root/.swiftlint.yml"
+test -x "$repo_root/scripts/swift-quality.sh"
+test -x "$repo_root/scripts/linux-shared-rust.sh"
+grep -Fq -- '--no-cache' "$repo_root/scripts/swift-quality.sh"
+grep -Fq 'swift-quality: doctor' "$repo_root/Makefile"
+grep -Fq 'linux-shared-rust: doctor' "$repo_root/Makefile"
+grep -Fq 'verify: swift-quality linux-shared-rust' "$repo_root/Makefile"
if rg -n \
'AsyncImage|URLSession\.shared|Data\(contentsOf:[[:space:]]*URL' \
diff --git a/scripts/xcode.sh b/scripts/xcode.sh
@@ -165,14 +165,16 @@ case "$operation" in
five-flow)
test_selector=RadrootsUITests/RadrootsRemoteQualificationUITests/testLocalSocialFiveFlowScenario
evidence_command=verify
- simulator_id=
+ simulator_id=${destination##*id=}
previous_content_size=
+ requested_content_size=large
;;
accessibility)
test_selector=RadrootsUITests/RadrootsRemoteQualificationUITests/testLocalSocialAccessibilitySemantics
evidence_command=verify-accessibility
simulator_id=${destination##*id=}
previous_content_size=
+ requested_content_size=accessibility-extra-extra-extra-large
persona_fixture=
persona_result=
;;
@@ -181,6 +183,7 @@ case "$operation" in
evidence_command=verify-persona
simulator_id=${destination##*id=}
previous_content_size=
+ requested_content_size=accessibility-extra-extra-extra-large
persona_fixture=test-fixtures/local-social-personas.v1.json
persona_result="$XCODE_RESULTS/$result_name.persona-result.json"
;;
@@ -261,19 +264,17 @@ case "$operation" in
echo "error: local-social fixture readiness timed out" >&2
exit 1
fi
- if [[ "$scenario" == accessibility || "$scenario" == persona ]]; then
- xcrun simctl boot "$simulator_id" >/dev/null 2>&1 || true
- xcrun simctl bootstatus "$simulator_id" -b >/dev/null
- previous_content_size=$(xcrun simctl ui "$simulator_id" content_size)
- case "$previous_content_size" in
- extra-small|small|medium|large|extra-large|extra-extra-large|extra-extra-extra-large|accessibility-medium|accessibility-large|accessibility-extra-large|accessibility-extra-extra-large|accessibility-extra-extra-extra-large) ;;
- *)
- echo "error: simulator content-size state is unavailable" >&2
- exit 1
- ;;
- esac
- xcrun simctl ui "$simulator_id" content_size accessibility-extra-extra-extra-large
- fi
+ xcrun simctl boot "$simulator_id" >/dev/null 2>&1 || true
+ xcrun simctl bootstatus "$simulator_id" -b >/dev/null
+ previous_content_size=$(xcrun simctl ui "$simulator_id" content_size)
+ case "$previous_content_size" in
+ extra-small|small|medium|large|extra-large|extra-extra-large|extra-extra-extra-large|accessibility-medium|accessibility-large|accessibility-extra-large|accessibility-extra-extra-large|accessibility-extra-extra-extra-large) ;;
+ *)
+ echo "error: simulator content-size state is unavailable" >&2
+ exit 1
+ ;;
+ esac
+ xcrun simctl ui "$simulator_id" content_size "$requested_content_size"
set +e
xcodebuild \
-project Radroots.xcodeproj \