field_ios

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

TeraStateMigrationTests.swift (25035B)


      1 import Foundation
      2 import RadrootsKit
      3 @testable import TeraApp
      4 import XCTest
      5 
      6 final class TeraStateMigrationTests: XCTestCase {
      7     func testRelayValidationMatchesRuntimeProfiles() throws {
      8         XCTAssertEqual(
      9           try TeraNetworkValidator.relays(
     10             ["wss://radroots.org", "WSS://WRITE.EXAMPLE:443/"],
     11             profile: .publicNetwork
     12           ),
     13           ["wss://write.example"]
     14         )
     15         XCTAssertEqual(
     16           try TeraNetworkValidator.relays(
     17             ["ws://127.0.0.1:7447"],
     18             profile: .simulator
     19           ),
     20           ["ws://127.0.0.1:7447"]
     21         )
     22         for accepted in [
     23           "ws://10.0.0.5:7447",
     24           "ws://172.16.0.5:7447",
     25           "ws://172.31.255.254:7447",
     26           "ws://192.168.0.5:7447",
     27           "ws://[fc00::5]:7447",
     28           "ws://[fd00::5]:7447",
     29           "wss://10.0.0.5:7447",
     30         ] {
     31             XCTAssertNoThrow(
     32               try TeraNetworkValidator.relays([accepted], profile: .device),
     33               "Expected device policy to admit \(accepted)"
     34             )
     35         }
     36         for denied in [
     37           "ws://8.8.8.8:7447",
     38           "ws://device.example:7447",
     39           "wss://device.example:7447",
     40           "ws://0.0.0.0:7447",
     41           "ws://127.0.0.1:7447",
     42           "ws://169.254.1.1:7447",
     43           "ws://172.32.0.5:7447",
     44           "ws://100.64.0.5:7447",
     45           "ws://224.0.0.1:7447",
     46           "ws://[::]:7447",
     47           "ws://[::1]:7447",
     48           "ws://[fe80::1]:7447",
     49           "ws://[ff02::1]:7447",
     50           "ws://[2001:4860:4860::8888]:7447",
     51         ] {
     52             XCTAssertThrowsError(
     53               try TeraNetworkValidator.relays([denied], profile: .device),
     54               "Expected device policy to deny \(denied)"
     55             )
     56         }
     57         for denied in [
     58           "ws://public.example",
     59           "wss://localhost",
     60           "wss://10.0.0.1",
     61           "wss://user@example.com",
     62           "wss://relay.example?token=value",
     63         ] {
     64             XCTAssertThrowsError(
     65               try TeraNetworkValidator.relays([denied], profile: .publicNetwork),
     66               "Expected public policy to deny \(denied)"
     67             )
     68         }
     69     }
     70 
     71     func testLegacyRelayMigrationIsIdempotentAndCorruptionIsNotAbsence() async throws {
     72         let fixture = try StateFixture()
     73         defer { fixture.remove() }
     74         let fileAccess = RadrootsAppleFileAccess(roots: fixture.roots)
     75         try fileAccess.write(
     76           .inline(
     77                 Data(
     78                     """
     79                     {"format":"radroots_field_ios_relay_settings_v1","relays":["ws://127.0.0.1:7447"]}
     80                     """.utf8
     81                 )
     82           ),
     83           to: RadrootsFileReference(
     84             scope: .data,
     85             relativePath: "settings/relay_settings.json"
     86           )
     87         )
     88         let store = TeraConfigurationStore(
     89           bootstrap: fixture.bootstrap,
     90           roots: fixture.roots
     91         )
     92         let first = try await store.load()
     93         let second = try await store.load()
     94         XCTAssertEqual(first, second)
     95         XCTAssertEqual(first.writableRelays, ["ws://127.0.0.1:7447"])
     96 
     97         try fileAccess.write(
     98           .inline(Data("not-json".utf8)),
     99           to: RadrootsFileReference(
    100             scope: .data,
    101             relativePath: "settings/radroots_configuration_v3.json"
    102           )
    103         )
    104         do {
    105             _ = try await store.load()
    106             XCTFail("Corrupt stored configuration must fail closed")
    107         } catch {
    108             XCTAssertEqual(error as? TeraConfigurationError, .corruptStoredConfiguration)
    109         }
    110     }
    111 
    112     func testV2ConfigurationMigratesOnceToExplicitV3BlossomAuthority() async throws {
    113         let fixture = try StateFixture()
    114         defer { fixture.remove() }
    115         let fileAccess = RadrootsAppleFileAccess(roots: fixture.roots)
    116         try fileAccess.write(
    117           .inline(
    118                 Data(
    119                     """
    120                     {"format":"radroots_ios_configuration_v2","profile":"simulator","writableRelays":["ws://127.0.0.1:7447"],"blossomOrigins":["http://127.0.0.1:3000","http://localhost:3001"]}
    121                     """.utf8
    122                 )
    123           ),
    124           to: RadrootsFileReference(
    125             scope: .data,
    126             relativePath: "settings/radroots_configuration_v2.json"
    127           )
    128         )
    129         let store = TeraConfigurationStore(bootstrap: fixture.bootstrap, roots: fixture.roots)
    130 
    131         let first = try await store.load()
    132         let second = try await store.load()
    133 
    134         XCTAssertEqual(first, second)
    135         XCTAssertEqual(
    136           first.blossom,
    137           TeraBlossomEndpointConfiguration(
    138             hostKind: .simulator,
    139             endpointAuthority: .loopbackDevelopment,
    140             primaryOrigin: "http://127.0.0.1:3000",
    141             fallbackOrigins: ["http://localhost:3001"]
    142           )
    143         )
    144         let persisted = try configurationObject(fileAccess)
    145         XCTAssertEqual(persisted["format"] as? String, "radroots_ios_configuration_v3")
    146         let blossom = try XCTUnwrap(persisted["blossom"] as? [String: Any])
    147         XCTAssertEqual(blossom["hostKind"] as? String, "simulator")
    148         XCTAssertEqual(blossom["endpointAuthority"] as? String, "loopback_development")
    149         XCTAssertEqual(persisted["activationState"] as? String, "current")
    150         XCTAssertEqual(persisted["generation"] as? UInt64, 1)
    151 
    152         let fingerprint = String(repeating: "a", count: 64)
    153         try await store.confirmCanonicalBlossomConfiguration(
    154           canonicalBlossomConfiguration(
    155             primaryOrigin: "http://127.0.0.1:3000",
    156             fallbackOrigins: ["http://localhost:3001"],
    157             fingerprint: fingerprint
    158           ),
    159           expectedGeneration: first.generation
    160         )
    161         let confirmed = try configurationObject(fileAccess)
    162         XCTAssertEqual(confirmed["canonicalBlossomConfigFingerprint"] as? String, fingerprint)
    163         XCTAssertEqual(confirmed["activationState"] as? String, "current")
    164     }
    165 
    166     func testStoredProfileDriftRecoversFromCurrentBootstrap() async throws {
    167         let fixture = try StateFixture()
    168         defer { fixture.remove() }
    169         let fileAccess = RadrootsAppleFileAccess(roots: fixture.roots)
    170         _ = try await TeraConfigurationStore(
    171           bootstrap: fixture.bootstrap,
    172           roots: fixture.roots
    173         ).load()
    174         let originalFingerprint = String(repeating: "b", count: 64)
    175         let originalStore = TeraConfigurationStore(
    176           bootstrap: fixture.bootstrap,
    177           roots: fixture.roots
    178         )
    179         try await originalStore.confirmCanonicalBlossomConfiguration(
    180           canonicalBlossomConfiguration(fingerprint: originalFingerprint),
    181           expectedGeneration: 1
    182         )
    183         let production = TeraConfigurationBootstrap(
    184           runtimeMode: "production",
    185           relayURLs: ["wss://write.example"],
    186           blossomOrigins: ["https://blossom.example"],
    187           keychainServicePrefix: fixture.bootstrap.keychainServicePrefix,
    188           bundleIdentifier: fixture.bootstrap.bundleIdentifier,
    189           appMetadata: fixture.bootstrap.appMetadata
    190         )
    191         let store = TeraConfigurationStore(bootstrap: production, roots: fixture.roots)
    192 
    193         let recovered = try await store.load()
    194 
    195         XCTAssertEqual(recovered.profile, .publicNetwork)
    196         XCTAssertEqual(recovered.activationState, .reconfigurationRequired)
    197         XCTAssertEqual(recovered.generation, 2)
    198         XCTAssertEqual(recovered.previousBlossomConfigFingerprint, originalFingerprint)
    199         XCTAssertEqual(recovered.writableRelays, ["wss://write.example"])
    200         XCTAssertEqual(
    201           recovered.blossom,
    202           TeraBlossomEndpointConfiguration(
    203             hostKind: .physicalDevice,
    204             endpointAuthority: .publicWebPKI,
    205             primaryOrigin: "https://blossom.example",
    206             fallbackOrigins: []
    207           )
    208         )
    209         let repeated = try await store.load()
    210         XCTAssertEqual(repeated, recovered)
    211 
    212         let persisted = try configurationObject(fileAccess)
    213         XCTAssertEqual(persisted["profile"] as? String, "public")
    214         XCTAssertEqual(persisted["activationState"] as? String, "reconfiguration_required")
    215         XCTAssertNil(persisted["canonicalBlossomConfigFingerprint"])
    216         XCTAssertEqual(
    217           persisted["previousBlossomConfigFingerprint"] as? String,
    218           originalFingerprint
    219         )
    220     }
    221 
    222     func testBootstrapInputDriftRequiresExplicitGenerationAwareActivation() async throws {
    223         let fixture = try StateFixture()
    224         defer { fixture.remove() }
    225         let original = TeraConfigurationStore(
    226           bootstrap: fixture.bootstrap,
    227           roots: fixture.roots
    228         )
    229         let first = try await original.load()
    230         let originalFingerprint = String(repeating: "c", count: 64)
    231         try await original.confirmCanonicalBlossomConfiguration(
    232           canonicalBlossomConfiguration(fingerprint: originalFingerprint),
    233           expectedGeneration: first.generation
    234         )
    235         let changedBootstrap = TeraConfigurationBootstrap(
    236           runtimeMode: fixture.bootstrap.runtimeMode,
    237           relayURLs: ["ws://127.0.0.1:7448"],
    238           blossomOrigins: ["http://127.0.0.1:3001"],
    239           keychainServicePrefix: fixture.bootstrap.keychainServicePrefix,
    240           bundleIdentifier: fixture.bootstrap.bundleIdentifier,
    241           appMetadata: fixture.bootstrap.appMetadata
    242         )
    243         let changed = TeraConfigurationStore(
    244           bootstrap: changedBootstrap,
    245           roots: fixture.roots
    246         )
    247 
    248         let pending = try await changed.load()
    249 
    250         XCTAssertEqual(pending.activationState, .reconfigurationRequired)
    251         XCTAssertEqual(pending.generation, first.generation + 1)
    252         XCTAssertEqual(pending.previousBlossomConfigFingerprint, originalFingerprint)
    253         XCTAssertEqual(pending.writableRelays, ["ws://127.0.0.1:7448"])
    254         XCTAssertEqual(pending.blossom?.primaryOrigin, "http://127.0.0.1:3001")
    255 
    256         let replacementFingerprint = String(repeating: "d", count: 64)
    257         try await changed.confirmCanonicalBlossomConfiguration(
    258           canonicalBlossomConfiguration(
    259             primaryOrigin: "http://127.0.0.1:3001",
    260             fingerprint: replacementFingerprint
    261           ),
    262           expectedGeneration: pending.generation
    263         )
    264         let active = try await changed.load()
    265         XCTAssertEqual(active.activationState, .current)
    266         XCTAssertEqual(active.generation, pending.generation)
    267         XCTAssertNil(active.previousBlossomConfigFingerprint)
    268         let persisted = try configurationObject(
    269             RadrootsAppleFileAccess(roots: fixture.roots)
    270         )
    271         XCTAssertEqual(
    272           persisted["canonicalBlossomConfigFingerprint"] as? String,
    273           replacementFingerprint
    274         )
    275     }
    276 
    277     func testMaximumStoredGenerationFailsAsCorruptionWithoutWrapping() async throws {
    278         let fixture = try StateFixture()
    279         defer { fixture.remove() }
    280         let fileAccess = RadrootsAppleFileAccess(roots: fixture.roots)
    281         _ = try await TeraConfigurationStore(
    282           bootstrap: fixture.bootstrap,
    283           roots: fixture.roots
    284         ).load()
    285         var persisted = try configurationObject(fileAccess)
    286         persisted["generation"] = NSNumber(value: UInt64.max)
    287         try fileAccess.write(
    288           .inline(JSONSerialization.data(withJSONObject: persisted, options: [.sortedKeys])),
    289           to: RadrootsFileReference(
    290             scope: .data,
    291             relativePath: "settings/radroots_configuration_v3.json"
    292           )
    293         )
    294         let changedBootstrap = TeraConfigurationBootstrap(
    295           runtimeMode: fixture.bootstrap.runtimeMode,
    296           relayURLs: ["ws://127.0.0.1:7448"],
    297           blossomOrigins: fixture.bootstrap.blossomOrigins,
    298           keychainServicePrefix: fixture.bootstrap.keychainServicePrefix,
    299           bundleIdentifier: fixture.bootstrap.bundleIdentifier,
    300           appMetadata: fixture.bootstrap.appMetadata
    301         )
    302         let changed = TeraConfigurationStore(
    303           bootstrap: changedBootstrap,
    304           roots: fixture.roots
    305         )
    306 
    307         do {
    308             _ = try await changed.load()
    309             XCTFail("Maximum persisted generation must fail closed")
    310         } catch {
    311             XCTAssertEqual(error as? TeraConfigurationError, .corruptStoredConfiguration)
    312         }
    313         XCTAssertEqual(try configurationObject(fileAccess)["generation"] as? UInt64, .max)
    314     }
    315 
    316     func testBootstrapActivationRetainsSelectedNetworkAndClearsPendingRollbackState() async throws {
    317         let fixture = try StateFixture()
    318         defer { fixture.remove() }
    319         let original = TeraConfigurationStore(
    320           bootstrap: fixture.bootstrap,
    321           roots: fixture.roots
    322         )
    323         let first = try await original.load()
    324         try await original.confirmCanonicalBlossomConfiguration(
    325           canonicalBlossomConfiguration(fingerprint: String(repeating: "f", count: 64)),
    326           expectedGeneration: first.generation
    327         )
    328         let changedBootstrap = TeraConfigurationBootstrap(
    329           runtimeMode: fixture.bootstrap.runtimeMode,
    330           relayURLs: ["ws://127.0.0.1:7448"],
    331           blossomOrigins: ["http://127.0.0.1:3001"],
    332           keychainServicePrefix: fixture.bootstrap.keychainServicePrefix,
    333           bundleIdentifier: fixture.bootstrap.bundleIdentifier,
    334           appMetadata: fixture.bootstrap.appMetadata
    335         )
    336         let changed = TeraConfigurationStore(
    337           bootstrap: changedBootstrap,
    338           roots: fixture.roots
    339         )
    340         let pending = try await changed.load()
    341 
    342         try await changed.confirmBootstrapActivation(expectedGeneration: pending.generation)
    343 
    344         let active = try await changed.load()
    345         XCTAssertEqual(active.activationState, .current)
    346         XCTAssertEqual(active.generation, pending.generation)
    347         XCTAssertEqual(active.writableRelays, ["ws://127.0.0.1:7448"])
    348         XCTAssertEqual(active.blossom?.primaryOrigin, "http://127.0.0.1:3001")
    349         XCTAssertNil(active.previousBlossomConfigFingerprint)
    350         let persisted = try configurationObject(
    351             RadrootsAppleFileAccess(roots: fixture.roots)
    352         )
    353         XCTAssertNil(persisted["canonicalBlossomConfigFingerprint"])
    354         XCTAssertNil(persisted["previousBlossomConfigFingerprint"])
    355     }
    356 
    357     func testCanonicalPublicWebPkiRuntimeLabelPreservesStoredFormat() async throws {
    358         let fixture = try StateFixture()
    359         defer { fixture.remove() }
    360         let bootstrap = TeraConfigurationBootstrap(
    361           runtimeMode: "production",
    362           relayURLs: [],
    363           blossomOrigins: ["https://media.example"],
    364           keychainServicePrefix: fixture.bootstrap.keychainServicePrefix,
    365           bundleIdentifier: fixture.bootstrap.bundleIdentifier,
    366           appMetadata: fixture.bootstrap.appMetadata
    367         )
    368         let store = TeraConfigurationStore(bootstrap: bootstrap, roots: fixture.roots)
    369         let selected = try await store.load()
    370 
    371         try await store.confirmCanonicalBlossomConfiguration(
    372           TeraBlossomConfigurationStatus(
    373             schemaVersion: 1,
    374             hostKind: "physical_device",
    375             endpointAuthority: "public_webpki",
    376             primaryOrigin: "https://media.example",
    377             fallbackOrigins: [],
    378             configFingerprint: String(repeating: "e", count: 64)
    379           ),
    380           expectedGeneration: selected.generation
    381         )
    382 
    383         let persisted = try configurationObject(
    384             RadrootsAppleFileAccess(roots: fixture.roots)
    385         )
    386         let blossom = try XCTUnwrap(persisted["blossom"] as? [String: Any])
    387         XCTAssertEqual(blossom["endpointAuthority"] as? String, "public_web_pki")
    388     }
    389 
    390     func testStoredBlossomAuthorityDriftRecoversWithinProfile() async throws {
    391         let fixture = try StateFixture()
    392         defer { fixture.remove() }
    393         let fileAccess = RadrootsAppleFileAccess(roots: fixture.roots)
    394         try fileAccess.write(
    395           .inline(
    396                 Data(
    397                     """
    398                     {"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":[]}}
    399                     """.utf8
    400                 )
    401           ),
    402           to: RadrootsFileReference(
    403             scope: .data,
    404             relativePath: "settings/radroots_configuration_v3.json"
    405           )
    406         )
    407         let store = TeraConfigurationStore(bootstrap: fixture.bootstrap, roots: fixture.roots)
    408 
    409         let recovered = try await store.load()
    410 
    411         XCTAssertEqual(
    412           recovered.blossom,
    413           TeraBlossomEndpointConfiguration(
    414             hostKind: .simulator,
    415             endpointAuthority: .loopbackDevelopment,
    416             primaryOrigin: "http://127.0.0.1:3000",
    417             fallbackOrigins: []
    418           )
    419         )
    420     }
    421 
    422     private func configurationObject(
    423         _ fileAccess: RadrootsAppleFileAccess
    424     ) throws -> [String: Any] {
    425         let source = try fileAccess.read(
    426           RadrootsFileReference(
    427             scope: .data,
    428             relativePath: "settings/radroots_configuration_v3.json"
    429           ),
    430           mode: .inline(maxBytes: TeraConfigurationStore.maximumStoredConfigurationBytes)
    431         )
    432         guard case let .inline(data) = source,
    433               let object = try JSONSerialization.jsonObject(with: data) as? [String: Any]
    434         else {
    435             throw TeraConfigurationError.persistenceFailed
    436         }
    437         return object
    438     }
    439 
    440     private func canonicalBlossomConfiguration(
    441       primaryOrigin: String = "http://127.0.0.1:3000",
    442       fallbackOrigins: [String] = [],
    443       fingerprint: String
    444     ) -> TeraBlossomConfigurationStatus {
    445         TeraBlossomConfigurationStatus(
    446           schemaVersion: 1,
    447           hostKind: "simulator",
    448           endpointAuthority: "loopback_development",
    449           primaryOrigin: primaryOrigin,
    450           fallbackOrigins: fallbackOrigins,
    451           configFingerprint: fingerprint
    452         )
    453     }
    454 
    455     func testSourceGenerationAndVisualIdentitySurviveStoreRecreation() async throws {
    456         let fixture = try StateFixture()
    457         defer { fixture.remove() }
    458         let firstStore = TeraConfigurationStore(
    459           bootstrap: fixture.bootstrap,
    460           roots: fixture.roots
    461         )
    462         let first = try await firstStore.sourceGeneration()
    463         let secondStore = TeraConfigurationStore(
    464           bootstrap: fixture.bootstrap,
    465           roots: fixture.roots
    466         )
    467         let second = try await secondStore.sourceGeneration()
    468         XCTAssertEqual(first, second)
    469 
    470         let key = String(repeating: "ab", count: 32)
    471         XCTAssertEqual(
    472           TeraStableVisualIdentity(publicKeyHex: key),
    473           TeraStableVisualIdentity(publicKeyHex: key)
    474         )
    475         XCTAssertNotEqual(
    476           TeraStableVisualIdentity(publicKeyHex: key).digestHex,
    477           TeraStableVisualIdentity(publicKeyHex: String(repeating: "cd", count: 32)).digestHex
    478         )
    479     }
    480 
    481     func testSourceGenerationRejectsInvalidInjectedClockWithoutPersistence() async throws {
    482         let fixture = try StateFixture()
    483         defer { fixture.remove() }
    484         for value in [TimeInterval.nan, -1, TimeInterval.greatestFiniteMagnitude, 0] {
    485             let store = TeraConfigurationStore(
    486               bootstrap: fixture.bootstrap,
    487               roots: fixture.roots,
    488               clock: TeraClock(now: { Date(timeIntervalSince1970: value) })
    489             )
    490             do {
    491                 _ = try await store.sourceGeneration()
    492                 XCTFail("Invalid clock value must fail closed")
    493             } catch {
    494                 XCTAssertEqual(error as? TeraConfigurationError, .persistenceFailed)
    495             }
    496         }
    497         let fileAccess = RadrootsAppleFileAccess(roots: fixture.roots)
    498         XCTAssertThrowsError(
    499           try fileAccess.read(
    500             RadrootsFileReference(
    501               scope: .data,
    502               relativePath: "state/source_generation_v1.json"
    503             ),
    504             mode: .inline(maxBytes: TeraConfigurationStore.maximumStoredConfigurationBytes)
    505           )
    506         )
    507     }
    508 
    509     func testLegacyIdentityMigrationIsTransactionalAndIdempotent() async throws {
    510         let secureStore = InMemorySecureStore()
    511         let metadataStore = InMemoryIdentityMetadataStore()
    512         let servicePrefix = "org.radroots.tests.identity.\(UUID().uuidString.lowercased())"
    513         let defaults = try XCTUnwrap(UserDefaults(suiteName: servicePrefix))
    514         defer {
    515             UserDefaults(suiteName: servicePrefix)?.removePersistentDomain(forName: servicePrefix)
    516         }
    517         let legacyKey = RadrootsSecureStoreKey(
    518           namespace: "nostr_identity",
    519           name: "selected_secret_hex"
    520         )
    521         try secureStore.put(
    522           Data(String(repeating: "01", count: 32).utf8),
    523           for: legacyKey,
    524           policy: .secureLocalSecret
    525         )
    526         let custody = try RadrootsIdentityCustody(
    527           configuration: RadrootsIdentityCustodyConfiguration(
    528             namespace: "radroots_identity_v1",
    529             secretPolicy: .secureLocalSecret
    530           ),
    531           secureStore: secureStore,
    532           metadataStore: metadataStore,
    533           userPresence: AllowingUserPresence()
    534         )
    535         let store = TeraIdentityStore(
    536           custody: custody,
    537           secureStore: secureStore,
    538           servicePrefix: servicePrefix,
    539           userDefaults: defaults
    540         )
    541         let first = try await store.loadAndMigrate()
    542         XCTAssertEqual(first.state, .recoveryRequired)
    543         XCTAssertTrue(try secureStore.contains(legacyKey))
    544         let migrated = try await store.recover()
    545         let second = try await store.loadAndMigrate()
    546         XCTAssertEqual(migrated.publicKeyHex, second.publicKeyHex)
    547         XCTAssertEqual(migrated.state, .unlocked)
    548         XCTAssertFalse(try secureStore.contains(legacyKey))
    549     }
    550 
    551     func testMalformedLegacyIdentityMetadataIsNotTreatedAsMissing() async throws {
    552         let secureStore = InMemorySecureStore()
    553         let servicePrefix = "org.radroots.tests.corrupt.\(UUID().uuidString.lowercased())"
    554         let defaults = try XCTUnwrap(UserDefaults(suiteName: servicePrefix))
    555         defer {
    556             UserDefaults(suiteName: servicePrefix)?.removePersistentDomain(forName: servicePrefix)
    557         }
    558         defaults.set(
    559           Data("not-json".utf8),
    560           forKey: "field_ios.identity.public_metadata.\(servicePrefix)"
    561         )
    562         let custody = try RadrootsIdentityCustody(
    563           configuration: RadrootsIdentityCustodyConfiguration(
    564             namespace: "radroots_identity_v1",
    565             secretPolicy: .secureLocalSecret
    566           ),
    567           secureStore: secureStore,
    568           metadataStore: InMemoryIdentityMetadataStore(),
    569           userPresence: AllowingUserPresence()
    570         )
    571         let store = TeraIdentityStore(
    572           custody: custody,
    573           secureStore: secureStore,
    574           servicePrefix: servicePrefix,
    575           userDefaults: defaults
    576         )
    577         do {
    578             _ = try await store.loadAndMigrate()
    579             XCTFail("Malformed legacy metadata must be classified as corrupt")
    580         } catch {
    581             XCTAssertEqual(error as? TeraIdentityStoreError, .corruptLegacyMetadata)
    582         }
    583     }
    584 
    585     func testRuntimeSignerUsesCanonicalOperationIDInsteadOfSignerRequestDigest() async throws {
    586         let secureStore = InMemorySecureStore()
    587         let custody = try RadrootsIdentityCustody(
    588           configuration: RadrootsIdentityCustodyConfiguration(
    589             namespace: "radroots_identity_v1",
    590             secretPolicy: .secureLocalSecret
    591           ),
    592           secureStore: secureStore,
    593           metadataStore: InMemoryIdentityMetadataStore(),
    594           userPresence: AllowingUserPresence()
    595         )
    596         let servicePrefix = "org.radroots.tests.signer.\(UUID().uuidString.lowercased())"
    597         let store = TeraIdentityStore(
    598           custody: custody,
    599           secureStore: secureStore,
    600           servicePrefix: servicePrefix
    601         )
    602         let identity = try await store.create()
    603         let signer = try await store.signer(for: identity)
    604         let operationID = UUID().uuidString.lowercased()
    605 
    606         let outcome = try await signer.sign(
    607             TeraRuntimeSigningRequest(
    608               operationID: operationID,
    609               signerRequestID: String(repeating: "ab", count: 32),
    610               publicKeyHex: XCTUnwrap(identity.publicKeyHex),
    611               purpose: .blossomUpload,
    612               deadlineUnixMilliseconds: UInt64(Date().timeIntervalSince1970 * 1000) + 60000,
    613               digest: Data(repeating: 0xCD, count: 32)
    614             )
    615         )
    616 
    617         guard case let .signed(signatureHex) = outcome else {
    618             return XCTFail("The custody signer rejected the canonical runtime operation ID")
    619         }
    620         XCTAssertEqual(signatureHex.count, 128)
    621     }
    622 }