Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
CKeySafeActionsTests.swift
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | import CKeyScanKit |
| 2 | import XCTest |
| 3 | |
| 4 | /// Safe Actions against the conformance vectors (tests/vectors/safe-actions-v1.json). |
| 5 | final class CKeySafeActionsTests: XCTestCase { |
| 6 | private struct Vector: Decodable { |
| 7 | struct Fields: Decodable { let scope: Int; let issuer: String; let app: String; let action: String; let key_id: String; let request_id: String } |
| 8 | let name: String |
| 9 | let payload: String |
| 10 | let public_key: String |
| 11 | let now: Double |
| 12 | let max_lifetime: UInt32 |
| 13 | let expect: String |
| 14 | let fields: Fields? |
| 15 | } |
| 16 | |
| 17 | private func vectors() throws -> [Vector] { |
| 18 | // Tests/CKeyScanKitTests -> repository root. |
| 19 | let root = URL(fileURLWithPath: #filePath).deletingLastPathComponent().deletingLastPathComponent() |
| 20 | .deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent() |
| 21 | let data = try Data(contentsOf: root.appendingPathComponent("tests/vectors/safe-actions-v1.json")) |
| 22 | struct File: Decodable { let vectors: [Vector] } |
| 23 | return try JSONDecoder().decode(File.self, from: data).vectors |
| 24 | } |
| 25 | |
| 26 | func testSameDeviceVectorsRunAndBadOnesAreRefused() async throws { |
| 27 | for v in try vectors() where v.fields?.scope == 0 || v.expect == "integrity" { |
| 28 | let ran = expectation(description: v.name) |
| 29 | ran.isInverted = v.expect != "ok" |
| 30 | let issuer = v.fields?.issuer ?? "citeory.com" |
| 31 | let actions = CKeySafeActions(appID: v.fields?.app ?? "com.viadere", maxLifetime: v.max_lifetime, |
| 32 | now: { Date(timeIntervalSince1970: v.now) }) |
| 33 | .trust(issuer: issuer, keyID: v.fields?.key_id ?? "2026-11", publicKeyHex: v.public_key) |
| 34 | .register(v.fields?.action ?? "open.library", issuers: [issuer]) { _ in ran.fulfill() } |
| 35 | let outcome = await actions.handle(v.payload) |
| 36 | switch outcome { |
| 37 | case .performed: XCTAssertEqual(v.expect, "ok", v.name) |
| 38 | case .rejected: XCTAssertNotEqual(v.expect, "ok", v.name) |
| 39 | default: XCTFail("\(v.name): unexpected \(outcome)") |
| 40 | } |
| 41 | await fulfillment(of: [ran], timeout: 0.5) |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | func testACodeWorksOnceAndNonSafeActionsAreLeftAlone() async throws { |
| 46 | let v = try XCTUnwrap(try vectors().first { $0.expect == "ok" && $0.fields?.scope == 0 }) |
| 47 | let fields = try XCTUnwrap(v.fields) |
| 48 | let actions = CKeySafeActions(appID: fields.app, now: { Date(timeIntervalSince1970: v.now) }) |
| 49 | .trust(issuer: fields.issuer, keyID: fields.key_id, publicKeyHex: v.public_key) |
| 50 | .register(fields.action, issuers: [fields.issuer]) { _ in } |
| 51 | if case .performed = await actions.handle(v.payload) {} else { XCTFail("first use runs") } |
| 52 | if case .rejected(let reason) = await actions.handle(v.payload) { XCTAssertTrue(reason.contains("already")) } else { XCTFail("second use is refused") } |
| 53 | if case .notASafeAction = await actions.handle("https://citeory.com") {} else { XCTFail("ordinary links are not Safe Actions") } |
| 54 | } |
| 55 | |
| 56 | func testCrossDeviceNeedsTheIssuerAndTheConfirmation() async throws { |
| 57 | let v = try XCTUnwrap(try vectors().first { $0.expect == "ok" && $0.fields?.scope == 1 }) |
| 58 | let fields = try XCTUnwrap(v.fields) |
| 59 | let ran = expectation(description: "must not run") |
| 60 | ran.isInverted = true |
| 61 | // No request_info: a cross-device action cannot run at all. |
| 62 | let actions = CKeySafeActions(appID: fields.app, now: { Date(timeIntervalSince1970: v.now) }) |
| 63 | .trust(issuer: fields.issuer, keyID: fields.key_id, publicKeyHex: v.public_key) |
| 64 | .register(fields.action, issuers: [fields.issuer], scope: .crossDevice) { _ in ran.fulfill() } |
| 65 | if case .rejected = await actions.handle(v.payload) {} else { XCTFail("refused without the issuer's request information") } |
| 66 | await fulfillment(of: [ran], timeout: 0.5) |
| 67 | } |
| 68 | } |