field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

TeraRemoteQualification.swift (7865B)


      1 import Foundation
      2 import RadrootsKit
      3 import TeraKitBindings
      4 
      5 enum TeraQualificationNetworkMode: Sendable, Equatable {
      6   case isolatedLoopback
      7   case publicEndpoint
      8 
      9   init(profile: String?) throws {
     10     switch profile {
     11     case "simulator": self = .isolatedLoopback
     12     case "public": self = .publicEndpoint
     13     default:
     14       throw TeraConfigurationError.invalid("qualification_network_profile")
     15     }
     16   }
     17 
     18   var runtimeMode: String {
     19     switch self {
     20     case .isolatedLoopback: "simulator"
     21     case .publicEndpoint: "production"
     22     }
     23   }
     24 
     25   var permitsAutomatedUserPresence: Bool {
     26     self == .isolatedLoopback
     27   }
     28 
     29   var permitsTestSecretPolicy: Bool {
     30     self == .isolatedLoopback
     31   }
     32 }
     33 
     34 struct TeraQualificationEndpoint: Sendable, Equatable {
     35   enum Role: Sendable {
     36     case relay
     37     case blossom
     38   }
     39 
     40   let rawValue: String
     41 
     42   init(_ raw: String, role: Role, mode: TeraQualificationNetworkMode) throws {
     43     guard let value = URLComponents(string: raw),
     44       let scheme = value.scheme?.lowercased(),
     45       let host = value.host?.lowercased(),
     46       !host.isEmpty,
     47       value.user == nil,
     48       value.password == nil,
     49       value.query == nil,
     50       value.fragment == nil
     51     else {
     52       throw TeraConfigurationError.invalid("qualification_endpoint")
     53     }
     54     let expectedScheme = switch (role, mode) {
     55     case (.relay, .isolatedLoopback): "ws"
     56     case (.blossom, .isolatedLoopback): "http"
     57     case (.relay, .publicEndpoint): "wss"
     58     case (.blossom, .publicEndpoint): "https"
     59     }
     60     let loopback = host == "127.0.0.1"
     61     let validHost = switch mode {
     62     case .isolatedLoopback:
     63       loopback
     64         && value.port.map { 1 ... 65535 ~= $0 } == true
     65         && (value.path.isEmpty || value.path == "/")
     66     case .publicEndpoint: !loopback && host != "::1" && host != "localhost"
     67     }
     68     guard scheme == expectedScheme, validHost else {
     69       throw TeraConfigurationError.invalid("qualification_endpoint_policy")
     70     }
     71     rawValue = raw
     72   }
     73 }
     74 
     75 struct TeraRemoteQualificationEnvironment: Sendable, Equatable {
     76   static let enabledKey = "TERA_IOS_UI_TEST_REMOTE"
     77   static let runIDKey = "TERA_IOS_UI_TEST_RUN_ID"
     78   static let relayURLsKey = "TERA_IOS_UI_TEST_NOSTR_RELAY_URLS"
     79   static let blossomOriginsKey = "TERA_IOS_UI_TEST_BLOSSOM_ORIGINS"
     80   static let mediaRelativePathKey = "TERA_IOS_UI_TEST_MEDIA_RELATIVE_PATH"
     81   static let networkProfileKey = "TERA_IOS_UI_TEST_NETWORK_PROFILE"
     82 
     83   let runID: String
     84   let relayURLs: [String]
     85   let blossomOrigins: [String]
     86   let mediaFile: RadrootsFileReference?
     87   let networkMode: TeraQualificationNetworkMode
     88 
     89   var runtimeMode: String {
     90     networkMode.runtimeMode
     91   }
     92 
     93   var automatesIdentity: Bool {
     94     networkMode.permitsAutomatedUserPresence && networkMode.permitsTestSecretPolicy
     95   }
     96 
     97   private static let mediaFixtureBase64 =
     98     "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
     99 
    100   var keychainServicePrefix: String {
    101     "org.radroots.ios.remote-qualification.\(runID)"
    102   }
    103 
    104   var identityMetadataKeyPrefix: String {
    105     "\(keychainServicePrefix).identity"
    106   }
    107 
    108   var backgroundTransferIdentifierSuffix: String {
    109     "remote-qualification.\(runID)"
    110   }
    111 
    112   func isolatedFileRoots(from base: RadrootsAppleFileRoots) throws
    113     -> RadrootsAppleFileRoots
    114   {
    115     try RadrootsAppleFileRoots(
    116       appIdentifier: base.appIdentifier,
    117       dataRoot: base.dataRoot.appendingPathComponent(runID, isDirectory: true),
    118       cacheRoot: base.cacheRoot.appendingPathComponent(runID, isDirectory: true),
    119       temporaryRoot: base.temporaryRoot.appendingPathComponent(runID, isDirectory: true)
    120     )
    121   }
    122 
    123   static func applicationFileRoots(appIdentifier: String) throws -> RadrootsAppleFileRoots {
    124     let base = try RadrootsAppleFileRoots.appContainer(appIdentifier: appIdentifier)
    125     #if DEBUG
    126       return try TeraDurableMediaRoots.selectingStaging(in: current()?.isolatedFileRoots(from: base) ?? base)
    127     #else
    128       return try TeraDurableMediaRoots.selectingStaging(in: base)
    129     #endif
    130   }
    131 
    132   static func backgroundTransferIdentifier(appIdentifier: String) throws -> String {
    133     #if DEBUG
    134       if let qualification = try current() {
    135         return "\(appIdentifier.lowercased()).\(qualification.backgroundTransferIdentifierSuffix)"
    136       }
    137     #endif
    138     return "\(appIdentifier.lowercased()).background.transfer"
    139   }
    140 
    141   static func mediaFixtureData() throws -> Data {
    142     guard let data = Data(base64Encoded: mediaFixtureBase64), !data.isEmpty else {
    143       throw TeraConfigurationError.invalid("qualification_media_fixture")
    144     }
    145     return data
    146   }
    147 
    148   #if DEBUG
    149     static func current(
    150       environment: [String: String] = ProcessInfo.processInfo.environment
    151     ) throws -> Self? {
    152       guard environment[enabledKey] == "1" else { return nil }
    153       let runID = try requiredRunID(environment[runIDKey])
    154       let relays = separatedValues(environment[relayURLsKey])
    155       let blossoms = separatedValues(environment[blossomOriginsKey])
    156       let networkMode = try TeraQualificationNetworkMode(
    157         profile: environment[networkProfileKey]
    158       )
    159       guard !relays.isEmpty, blossoms.count == 1 else {
    160         throw TeraConfigurationError.invalid("qualification_blossom_origin")
    161       }
    162       let relayEndpoints = try relays.map {
    163         try TeraQualificationEndpoint($0, role: .relay, mode: networkMode)
    164       }
    165       let blossomEndpoints = try blossoms.map {
    166         try TeraQualificationEndpoint($0, role: .blossom, mode: networkMode)
    167       }
    168       guard Set(relayEndpoints.map(\.rawValue)).count == relayEndpoints.count else {
    169         throw TeraConfigurationError.invalid("qualification_endpoint_duplicate")
    170       }
    171       let mediaFile = try environment[mediaRelativePathKey].map { raw in
    172         guard raw == "qualification/input.png" else {
    173           throw TeraConfigurationError.invalid("qualification_media_file")
    174         }
    175         return RadrootsFileReference(scope: .data, relativePath: raw)
    176       }
    177       return Self(
    178         runID: runID,
    179         relayURLs: relayEndpoints.map(\.rawValue),
    180         blossomOrigins: blossomEndpoints.map(\.rawValue),
    181         mediaFile: mediaFile,
    182         networkMode: networkMode
    183       )
    184     }
    185 
    186     private static func requiredRunID(_ raw: String?) throws -> String {
    187       guard let raw,
    188         (8 ... 64).contains(raw.utf8.count),
    189         raw == raw.lowercased(),
    190         raw.unicodeScalars.allSatisfy({
    191           CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyz0123456789-")
    192             .contains($0)
    193         }),
    194         raw.first != "-",
    195         raw.last != "-"
    196       else {
    197         throw TeraConfigurationError.invalid("qualification_run_id")
    198       }
    199       return raw
    200     }
    201 
    202     private static func separatedValues(_ raw: String?) -> [String] {
    203       guard let raw else { return [] }
    204       return raw.components(separatedBy: CharacterSet(charactersIn: ",; \n\r\t"))
    205         .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
    206         .filter { !$0.isEmpty }
    207     }
    208 
    209   #else
    210     static func current(environment _: [String: String] = [:]) throws -> Self? {
    211       nil
    212     }
    213   #endif
    214 }
    215 
    216 #if DEBUG
    217   final class TeraRemoteQualificationUserPresence: RadrootsUserPresence, Sendable {
    218     init(mode: TeraQualificationNetworkMode) throws {
    219       guard mode.permitsAutomatedUserPresence, mode.permitsTestSecretPolicy else {
    220         throw TeraConfigurationError.invalid("qualification_user_presence")
    221       }
    222     }
    223 
    224     func currentStatus() async throws -> RadrootsUserPresenceStatus {
    225       RadrootsUserPresenceStatus(
    226         support: .deviceCredential,
    227         biometryKind: .none,
    228         canEvaluateDeviceCredential: true,
    229         canEvaluateBiometrics: false
    230       )
    231     }
    232 
    233     func verify(
    234       _ request: RadrootsUserPresenceRequest
    235     ) async throws -> RadrootsUserPresenceResult {
    236       RadrootsUserPresenceResult(policy: request.policy, verified: true)
    237     }
    238   }
    239 #endif