// PairingTests.swift // Unit tests for PairingService.parseQR — IC-1 QR URL parsing. // // Format: pi-remote://:?pair=&fp=&name= // // 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 } } } }