Files
openclaw/apps/ios/Tests/DeepLinkParserTests.swift
PollyBot13 a320f775f0 fix(ios): defer QR pairing after scanner dismissal (#99572)
* fix(ios): defer QR pairing after scanner dismissal

* fix(ios): process QR pairing after scanner dismissal

* fix(ios): harden QR scanner handoff

* fix(ios): give QR scanner dismissal more time

* fix(ios): keep onboarding open for QR trust prompt

* fix(ios): keep QR trust prompt owned by onboarding

* fix(ios): recover operator pairing after QR bootstrap

* fix(ios): cancel stale QR scanner handoffs

Co-authored-by: PollyBot13 <pollybot13@gmail.com>

* fix(ios): defer QR setup until onboarding closes

* fix(ios): keep QR setup links with visible settings

* fix(ios): consume setup links during onboarding

* fix(ios): handle setup links during onboarding launch

* fix(ios): route setup links through active onboarding

* fix(ios): harden QR gateway handoff

* fix(ios): cancel superseded gateway attempts

* fix(ios): serialize scanner result delivery

* fix(ios): prevent stale gateway reconnects

* fix(ios): serialize gateway target handoff

* fix(ios): disable stale gateway relaunch route

* fix(ios): await staged bootstrap reset

* test(ios): bound gateway reset handoff

* fix(ios): preserve explicit gateway handoff

* fix(ios): harden gateway lifecycle ownership

* chore(ios): sync native i18n inventory

* test(ios): align gateway ownership assertions

* refactor(ios): remove superseded gateway helpers

* fix(ios): keep gateway auth route scoped

* fix(ios): restore gateway target review state

* fix(protocol): refresh Swift plugin approval model

* test(ios): isolate state directory overrides

* fix(ios): preserve watch alerts across gateway switches

* fix(ios): bind deferred work to gateway ownership

* docs(changelog): credit iOS gateway handoff fix

* chore(i18n): sync native app inventory

* test(ios): remove unused Watch approval hooks

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-05 12:31:53 -07:00

256 lines
8.6 KiB
Swift

import OpenClawKit
import Foundation
import Testing
private func setupCode(from payload: String) -> String {
Data(payload.utf8)
.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
private func agentAction(
message: String,
sessionKey: String? = nil,
thinking: String? = nil,
deliver: Bool = false,
to: String? = nil,
channel: String? = nil,
timeoutSeconds: Int? = nil,
key: String? = nil) -> DeepLinkRoute
{
.agent(
.init(
message: message,
sessionKey: sessionKey,
thinking: thinking,
deliver: deliver,
to: to,
channel: channel,
timeoutSeconds: timeoutSeconds,
key: key))
}
@Suite struct DeepLinkParserTests {
@Test func parseRejectsUnknownHost() {
let url = URL(string: "openclaw://nope?message=hi")!
#expect(DeepLinkParser.parse(url) == nil)
}
@Test func parseHostIsCaseInsensitive() {
let url = URL(string: "openclaw://AGENT?message=Hello")!
#expect(DeepLinkParser.parse(url) == agentAction(message: "Hello"))
}
@Test func parseRejectsNonOpenClawScheme() {
let url = URL(string: "https://example.com/agent?message=hi")!
#expect(DeepLinkParser.parse(url) == nil)
}
@Test func parseRejectsEmptyMessage() {
let url = URL(string: "openclaw://agent?message=%20%20%0A")!
#expect(DeepLinkParser.parse(url) == nil)
}
@Test func parseAgentLinkParsesCommonFields() {
let url =
URL(string: "openclaw://agent?message=Hello&deliver=1&sessionKey=node-test&thinking=low&timeoutSeconds=30")!
#expect(DeepLinkParser.parse(url) == agentAction(
message: "Hello",
sessionKey: "node-test",
thinking: "low",
deliver: true,
timeoutSeconds: 30))
}
@Test func parseAgentLinkParsesTargetRoutingFields() {
let url =
URL(
string: "openclaw://agent?message=Hello%20World&deliver=1&to=%2B15551234567&channel=whatsapp&key=secret")!
#expect(DeepLinkParser.parse(url) == agentAction(
message: "Hello World",
deliver: true,
to: "+15551234567",
channel: "whatsapp",
key: "secret"))
}
@Test func parseRejectsNegativeTimeoutSeconds() {
let url = URL(string: "openclaw://agent?message=Hello&timeoutSeconds=-1")!
#expect(DeepLinkParser.parse(url) == agentAction(message: "Hello"))
}
@Test func parseGatewayLinkParsesCommonFields() {
let url = URL(
string: "openclaw://gateway?host=openclaw.local&port=18789&tls=1&token=abc&password=def")!
#expect(
DeepLinkParser.parse(url) == .gateway(
.init(
host: "openclaw.local",
port: 18789,
tls: true,
bootstrapToken: nil,
token: "abc",
password: "def")))
}
@Test func parseGatewayLinkRejectsInsecureNonLoopbackWs() {
let url = URL(
string: "openclaw://gateway?host=attacker.example&port=18789&tls=0&token=abc")!
#expect(DeepLinkParser.parse(url) == nil)
}
@Test func parseGatewayLinkAllowsPrivateLanWs() {
let url = URL(
string: "openclaw://gateway?host=openclaw.local&port=18789&tls=0&token=abc")!
#expect(
DeepLinkParser.parse(url) == .gateway(
.init(
host: "openclaw.local",
port: 18789,
tls: false,
bootstrapToken: nil,
token: "abc",
password: nil)))
}
@Test func parseGatewayLinkRejectsInsecurePrefixBypassHost() {
let url = URL(
string: "openclaw://gateway?host=127.attacker.example&port=18789&tls=0&token=abc")!
#expect(DeepLinkParser.parse(url) == nil)
}
@Test func parseGatewayLinkRejectsInvalidPort() {
let url = URL(string: "openclaw://gateway?host=gateway.example.com&port=70000&tls=1")!
#expect(DeepLinkParser.parse(url) == nil)
}
@Test func parseGatewayLinkRejectsMalformedPort() {
let url = URL(string: "openclaw://gateway?host=gateway.example.com&port=not-a-port&tls=1")!
#expect(DeepLinkParser.parse(url) == nil)
}
@Test func parseGatewaySetupCodeParsesBase64UrlPayload() {
let payload = #"{"url":"wss://gateway.example.com:443","bootstrapToken":"tok","password":"pw"}"#
let link = GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload))
#expect(link == .init(
host: "gateway.example.com",
port: 443,
tls: true,
bootstrapToken: "tok",
token: nil,
password: "pw"))
}
@Test func parseGatewaySetupCodeRejectsInvalidInput() {
#expect(GatewayConnectDeepLink.fromSetupCode("not-a-valid-setup-code") == nil)
}
@Test func parseGatewaySetupCodeRejectsInvalidPort() {
let payload = #"{"host":"gateway.example.com","port":70000,"tls":true}"#
#expect(GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == nil)
}
@Test func invalidPortHasNoWebSocketURL() {
let link = GatewayConnectDeepLink(
host: "gateway.example.com",
port: -1,
tls: true,
bootstrapToken: nil,
token: nil,
password: nil)
#expect(link.websocketURL == nil)
#expect(!link.isValidEndpoint)
}
@Test func parseGatewaySetupCodeDefaultsTo443ForWssWithoutPort() {
let payload = #"{"url":"wss://gateway.example.com","bootstrapToken":"tok"}"#
let link = GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload))
#expect(link == .init(
host: "gateway.example.com",
port: 443,
tls: true,
bootstrapToken: "tok",
token: nil,
password: nil))
}
@Test func parseGatewaySetupCodeRejectsInsecureNonLoopbackWs() {
let payload = #"{"url":"ws://attacker.example:18789","bootstrapToken":"tok"}"#
let link = GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload))
#expect(link == nil)
}
@Test func parseGatewaySetupCodeRejectsInsecurePrefixBypassHost() {
let payload = #"{"url":"ws://127.attacker.example:18789","bootstrapToken":"tok"}"#
let link = GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload))
#expect(link == nil)
}
@Test func parseGatewaySetupCodeAllowsLoopbackWs() {
let payload = #"{"url":"ws://127.0.0.1:18789","bootstrapToken":"tok"}"#
let link = GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload))
#expect(link == .init(
host: "127.0.0.1",
port: 18789,
tls: false,
bootstrapToken: "tok",
token: nil,
password: nil))
}
@Test func parseGatewaySetupCodeAllowsPrivateLanWs() {
let payload = #"{"url":"ws://openclaw.local:18789","bootstrapToken":"tok"}"#
let link = GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload))
#expect(link == .init(
host: "openclaw.local",
port: 18789,
tls: false,
bootstrapToken: "tok",
token: nil,
password: nil))
}
@Test func parseGatewaySetupCodeRejectsTailnetPlaintextWs() {
let payload = #"{"url":"ws://gateway.tailnet.ts.net:18789","bootstrapToken":"tok"}"#
let link = GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload))
#expect(link == nil)
}
@Test func parseGatewaySetupInputParsesFullCopiedSetupMessage() {
let payload = #"{"url":"wss://gateway.example.com","bootstrapToken":"tok"}"#
let link = GatewayConnectDeepLink.fromSetupInput("""
Pairing setup code generated.
Setup code:
\(setupCode(from: payload))
""")
#expect(link == .init(
host: "gateway.example.com",
port: 443,
tls: true,
bootstrapToken: "tok",
token: nil,
password: nil))
}
@Test func parseGatewaySetupInputParsesRawGatewayURL() {
let link = GatewayConnectDeepLink.fromSetupInput("wss://gateway.example.com:444")
#expect(link == .init(
host: "gateway.example.com",
port: 444,
tls: true,
bootstrapToken: nil,
token: nil,
password: nil))
}
}