pi-remote-ios/Tests/CoreTests/PairingTests.swift

161 lines
6.5 KiB
Swift

// PairingTests.swift
// Unit tests for PairingService.parseQR IC-1 QR URL parsing.
//
// Format: pi-remote://<host>:<port>?pair=<pairingToken>&fp=<fingerprint>&name=<sidecarName>
//
// No network calls are made; parseQR is a pure function.
import XCTest
@testable import piRemote
final class PairingTests: XCTestCase {
// =========================================================================
// MARK: 1. Happy-path parsing
// =========================================================================
func testParseQR_canonical_parsesAllFields() throws {
let url = "pi-remote://192.168.1.1:7777?pair=abc&fp=deadbeef&name=pi-remote"
let result = try PairingService.parseQR(url)
XCTAssertEqual(result.host, "192.168.1.1")
XCTAssertEqual(result.port, 7777)
XCTAssertEqual(result.pairingToken, "abc")
XCTAssertEqual(result.fingerprint, "deadbeef")
XCTAssertEqual(result.name, "pi-remote")
}
func testParseQR_hostOnly_differentPort() throws {
let url = "pi-remote://pi.local:9000?pair=tok123&fp=aabbcc&name=mypi"
let result = try PairingService.parseQR(url)
XCTAssertEqual(result.host, "pi.local")
XCTAssertEqual(result.port, 9000)
XCTAssertEqual(result.pairingToken, "tok123")
XCTAssertEqual(result.fingerprint, "aabbcc")
XCTAssertEqual(result.name, "mypi")
}
func testParseQR_longFingerprint_parsedCorrectly() throws {
let fp = String(repeating: "a1", count: 32) // 64-char SHA-256 hex
let url = "pi-remote://10.0.0.1:7777?pair=t&fp=\(fp)&name=n"
let result = try PairingService.parseQR(url)
XCTAssertEqual(result.fingerprint, fp)
}
func testParseQR_portBoundary_lowPort() throws {
let url = "pi-remote://localhost:1?pair=t&fp=f&name=n"
let result = try PairingService.parseQR(url)
XCTAssertEqual(result.port, 1)
}
func testParseQR_portBoundary_highPort() throws {
let url = "pi-remote://localhost:65535?pair=t&fp=f&name=n"
let result = try PairingService.parseQR(url)
XCTAssertEqual(result.port, 65535)
}
func testParseQR_nameWithSpaces_percentEncoded() throws {
// Spaces encoded as %20 in the QR URL
let url = "pi-remote://192.168.1.1:7777?pair=tok&fp=fp&name=my%20pi"
let result = try PairingService.parseQR(url)
XCTAssertEqual(result.name, "my pi")
}
// =========================================================================
// MARK: 2. Missing required parameters throws
// =========================================================================
func testParseQR_missingPairParam_throws() {
let url = "pi-remote://192.168.1.1:7777?fp=deadbeef&name=pi-remote"
XCTAssertThrowsError(try PairingService.parseQR(url),
"Missing 'pair' must throw")
}
func testParseQR_missingFpParam_throws() {
let url = "pi-remote://192.168.1.1:7777?pair=abc&name=pi-remote"
XCTAssertThrowsError(try PairingService.parseQR(url),
"Missing 'fp' must throw")
}
func testParseQR_missingNameParam_throws() {
let url = "pi-remote://192.168.1.1:7777?pair=abc&fp=deadbeef"
XCTAssertThrowsError(try PairingService.parseQR(url),
"Missing 'name' must throw")
}
func testParseQR_noQueryParams_throws() {
let url = "pi-remote://192.168.1.1:7777"
XCTAssertThrowsError(try PairingService.parseQR(url),
"No query params must throw")
}
func testParseQR_emptyPairValue_throws() {
let url = "pi-remote://192.168.1.1:7777?pair=&fp=deadbeef&name=pi-remote"
XCTAssertThrowsError(try PairingService.parseQR(url),
"Empty 'pair' value must throw")
}
// =========================================================================
// MARK: 3. Wrong scheme throws
// =========================================================================
func testParseQR_httpsScheme_throws() {
// Wrong scheme sidecar only issues pi-remote:// URLs
let url = "https://192.168.1.1:7777?pair=abc&fp=deadbeef&name=pi-remote"
XCTAssertThrowsError(try PairingService.parseQR(url),
"https:// scheme must throw .invalidQR")
}
func testParseQR_httpScheme_throws() {
let url = "http://192.168.1.1:7777?pair=abc&fp=deadbeef&name=pi-remote"
XCTAssertThrowsError(try PairingService.parseQR(url),
"http:// scheme must throw .invalidQR")
}
func testParseQR_emptyString_throws() {
XCTAssertThrowsError(try PairingService.parseQR(""),
"Empty string must throw")
}
func testParseQR_randomString_throws() {
XCTAssertThrowsError(try PairingService.parseQR("not-a-url"),
"Garbage string must throw")
}
// =========================================================================
// MARK: 4. Missing port throws
// =========================================================================
func testParseQR_missingPort_throws() {
// No port specified should throw because port is required
let url = "pi-remote://192.168.1.1?pair=abc&fp=deadbeef&name=pi-remote"
XCTAssertThrowsError(try PairingService.parseQR(url),
"Missing port must throw .invalidQR")
}
// =========================================================================
// MARK: 5. Error type is PairingError.invalidQR
// =========================================================================
func testParseQR_wrongScheme_throwsInvalidQR() {
XCTAssertThrowsError(try PairingService.parseQR("https://host:7777?pair=x&fp=y&name=z")) { error in
guard let pairingError = error as? PairingError,
case .invalidQR = pairingError else {
XCTFail("Expected PairingError.invalidQR, got \(error)")
return
}
}
}
func testParseQR_missingParam_throwsInvalidQR() {
XCTAssertThrowsError(try PairingService.parseQR("pi-remote://host:7777?pair=x&fp=y")) { error in
guard let pairingError = error as? PairingError,
case .invalidQR = pairingError else {
XCTFail("Expected PairingError.invalidQR, got \(error)")
return
}
}
}
}