204 lines
8.3 KiB
Swift
204 lines
8.3 KiB
Swift
import XCTest
|
|
|
|
#if canImport(cmux)
|
|
@testable import cmux
|
|
#elseif canImport(cmux_DEV)
|
|
@testable import cmux_DEV
|
|
#endif
|
|
|
|
final class WorkspaceRemoteConnectionTests: XCTestCase {
|
|
private struct ProcessRunResult {
|
|
let status: Int32
|
|
let stdout: String
|
|
let stderr: String
|
|
let timedOut: Bool
|
|
}
|
|
|
|
private func runProcess(
|
|
executablePath: String,
|
|
arguments: [String],
|
|
timeout: TimeInterval
|
|
) -> ProcessRunResult {
|
|
let process = Process()
|
|
let stdoutPipe = Pipe()
|
|
let stderrPipe = Pipe()
|
|
process.executableURL = URL(fileURLWithPath: executablePath)
|
|
process.arguments = arguments
|
|
process.standardInput = FileHandle.nullDevice
|
|
process.standardOutput = stdoutPipe
|
|
process.standardError = stderrPipe
|
|
|
|
do {
|
|
try process.run()
|
|
} catch {
|
|
return ProcessRunResult(
|
|
status: -1,
|
|
stdout: "",
|
|
stderr: String(describing: error),
|
|
timedOut: false
|
|
)
|
|
}
|
|
|
|
let exitSignal = DispatchSemaphore(value: 0)
|
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
process.waitUntilExit()
|
|
exitSignal.signal()
|
|
}
|
|
|
|
let timedOut = exitSignal.wait(timeout: .now() + timeout) == .timedOut
|
|
if timedOut {
|
|
process.terminate()
|
|
_ = exitSignal.wait(timeout: .now() + 1)
|
|
}
|
|
|
|
let stdout = String(data: stdoutPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
|
|
let stderr = String(data: stderrPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
|
|
return ProcessRunResult(
|
|
status: process.terminationStatus,
|
|
stdout: stdout,
|
|
stderr: stderr,
|
|
timedOut: timedOut
|
|
)
|
|
}
|
|
|
|
func testRemoteRelayMetadataCleanupScriptRemovesMatchingSocketAddr() {
|
|
let fileManager = FileManager.default
|
|
let home = fileManager.temporaryDirectory.appendingPathComponent("cmux-relay-cleanup-\(UUID().uuidString)")
|
|
let relayDir = home.appendingPathComponent(".cmux/relay")
|
|
let socketAddrURL = home.appendingPathComponent(".cmux/socket_addr")
|
|
let authURL = relayDir.appendingPathComponent("64008.auth")
|
|
let daemonPathURL = relayDir.appendingPathComponent("64008.daemon_path")
|
|
|
|
XCTAssertNoThrow(try fileManager.createDirectory(at: relayDir, withIntermediateDirectories: true))
|
|
XCTAssertNoThrow(try "127.0.0.1:64008".write(to: socketAddrURL, atomically: true, encoding: .utf8))
|
|
XCTAssertNoThrow(try "auth".write(to: authURL, atomically: true, encoding: .utf8))
|
|
XCTAssertNoThrow(try "daemon".write(to: daemonPathURL, atomically: true, encoding: .utf8))
|
|
defer { try? fileManager.removeItem(at: home) }
|
|
|
|
let result = runProcess(
|
|
executablePath: "/usr/bin/env",
|
|
arguments: [
|
|
"HOME=\(home.path)",
|
|
"/bin/sh",
|
|
"-c",
|
|
WorkspaceRemoteSessionController.remoteRelayMetadataCleanupScript(relayPort: 64008),
|
|
],
|
|
timeout: 5
|
|
)
|
|
|
|
XCTAssertFalse(result.timedOut, result.stderr)
|
|
XCTAssertEqual(result.status, 0, result.stderr)
|
|
XCTAssertFalse(fileManager.fileExists(atPath: socketAddrURL.path))
|
|
XCTAssertFalse(fileManager.fileExists(atPath: authURL.path))
|
|
XCTAssertFalse(fileManager.fileExists(atPath: daemonPathURL.path))
|
|
}
|
|
|
|
func testRemoteRelayMetadataCleanupScriptPreservesDifferentSocketAddr() {
|
|
let fileManager = FileManager.default
|
|
let home = fileManager.temporaryDirectory.appendingPathComponent("cmux-relay-cleanup-preserve-\(UUID().uuidString)")
|
|
let relayDir = home.appendingPathComponent(".cmux/relay")
|
|
let socketAddrURL = home.appendingPathComponent(".cmux/socket_addr")
|
|
let authURL = relayDir.appendingPathComponent("64009.auth")
|
|
let daemonPathURL = relayDir.appendingPathComponent("64009.daemon_path")
|
|
|
|
XCTAssertNoThrow(try fileManager.createDirectory(at: relayDir, withIntermediateDirectories: true))
|
|
XCTAssertNoThrow(try "127.0.0.1:64010".write(to: socketAddrURL, atomically: true, encoding: .utf8))
|
|
XCTAssertNoThrow(try "auth".write(to: authURL, atomically: true, encoding: .utf8))
|
|
XCTAssertNoThrow(try "daemon".write(to: daemonPathURL, atomically: true, encoding: .utf8))
|
|
defer { try? fileManager.removeItem(at: home) }
|
|
|
|
let result = runProcess(
|
|
executablePath: "/usr/bin/env",
|
|
arguments: [
|
|
"HOME=\(home.path)",
|
|
"/bin/sh",
|
|
"-c",
|
|
WorkspaceRemoteSessionController.remoteRelayMetadataCleanupScript(relayPort: 64009),
|
|
],
|
|
timeout: 5
|
|
)
|
|
|
|
XCTAssertFalse(result.timedOut, result.stderr)
|
|
XCTAssertEqual(result.status, 0, result.stderr)
|
|
XCTAssertTrue(fileManager.fileExists(atPath: socketAddrURL.path))
|
|
XCTAssertFalse(fileManager.fileExists(atPath: authURL.path))
|
|
XCTAssertFalse(fileManager.fileExists(atPath: daemonPathURL.path))
|
|
}
|
|
|
|
func testReverseRelayStartupFailureDetailCapturesImmediateForwardingFailure() throws {
|
|
let process = Process()
|
|
let stderrPipe = Pipe()
|
|
process.executableURL = URL(fileURLWithPath: "/bin/sh")
|
|
process.arguments = ["-c", "echo 'remote port forwarding failed for listen port 64009' >&2; exit 1"]
|
|
process.standardInput = FileHandle.nullDevice
|
|
process.standardOutput = FileHandle.nullDevice
|
|
process.standardError = stderrPipe
|
|
|
|
try process.run()
|
|
|
|
let detail = WorkspaceRemoteSessionController.reverseRelayStartupFailureDetail(
|
|
process: process,
|
|
stderrPipe: stderrPipe,
|
|
gracePeriod: 1.0
|
|
)
|
|
|
|
XCTAssertEqual(detail, "remote port forwarding failed for listen port 64009")
|
|
}
|
|
|
|
@MainActor
|
|
func testProxyOnlyErrorsKeepSSHWorkspaceConnectedAndLoggedInSidebar() {
|
|
let workspace = Workspace()
|
|
let config = WorkspaceRemoteConfiguration(
|
|
destination: "cmux-macmini",
|
|
port: nil,
|
|
identityFile: nil,
|
|
sshOptions: [],
|
|
localProxyPort: nil,
|
|
relayPort: 64007,
|
|
relayID: String(repeating: "a", count: 16),
|
|
relayToken: String(repeating: "b", count: 64),
|
|
localSocketPath: "/tmp/cmux-debug-test.sock",
|
|
terminalStartupCommand: "ssh cmux-macmini"
|
|
)
|
|
|
|
workspace.configureRemoteConnection(config, autoConnect: false)
|
|
XCTAssertEqual(workspace.activeRemoteTerminalSessionCount, 1)
|
|
|
|
let proxyError = "Remote proxy to cmux-macmini unavailable: Failed to start local daemon proxy: daemon RPC timeout waiting for hello response (retry in 3s)"
|
|
workspace.applyRemoteConnectionStateUpdate(.error, detail: proxyError, target: "cmux-macmini")
|
|
|
|
XCTAssertEqual(workspace.remoteConnectionState, .connected)
|
|
XCTAssertEqual(workspace.remoteConnectionDetail, proxyError)
|
|
XCTAssertEqual(
|
|
workspace.statusEntries["remote.error"]?.value,
|
|
"Remote proxy unavailable (cmux-macmini): \(proxyError)"
|
|
)
|
|
XCTAssertEqual(workspace.logEntries.last?.source, "remote-proxy")
|
|
XCTAssertEqual(workspace.remoteStatusPayload()["connected"] as? Bool, true)
|
|
XCTAssertEqual(
|
|
((workspace.remoteStatusPayload()["proxy"] as? [String: Any])?["state"] as? String),
|
|
"error"
|
|
)
|
|
|
|
workspace.applyRemoteConnectionStateUpdate(.connecting, detail: "Connecting to cmux-macmini", target: "cmux-macmini")
|
|
|
|
XCTAssertEqual(workspace.remoteConnectionState, .connected)
|
|
XCTAssertEqual(
|
|
workspace.statusEntries["remote.error"]?.value,
|
|
"Remote proxy unavailable (cmux-macmini): \(proxyError)"
|
|
)
|
|
|
|
workspace.applyRemoteConnectionStateUpdate(
|
|
.connected,
|
|
detail: "Connected to cmux-macmini via shared local proxy 127.0.0.1:9999",
|
|
target: "cmux-macmini"
|
|
)
|
|
|
|
XCTAssertEqual(workspace.remoteConnectionState, .connected)
|
|
XCTAssertNil(workspace.statusEntries["remote.error"])
|
|
XCTAssertEqual(
|
|
((workspace.remoteStatusPayload()["proxy"] as? [String: Any])?["state"] as? String),
|
|
"unavailable"
|
|
)
|
|
}
|
|
}
|