swift-sdk/Tests/MCPTests/NetworkTransportTests.swift
Maksym Mova 6112a3995a
V0.11.x (#199)
* Support 2025-06-18 Spec (#167)

* Support OpenAI _meta for Tool

* Update init

* Add title and outputSchema

* Concurreny issue

* General fields

* Add Generate Fields to Server elements

* 4 tab spacing

* Update Version

* Elicitation

* Format

* More tests and inits

* Elicitation in README

* Fix orphaned test

* Add title to more structs

* Public init

* Fix meta validation and make tests more generic

* Format and make Result properties public again

* Revert more visibility changes

* feat: 2025-11-25 phase 1, includes Version, Icon, Progress (#181)

* chore: fixed unit tests

* feat: added icon and progess base structs

* feat: added progress notification and meta to callTool

* feat: added tests

* feat: simplified RequestMeta encoding/decoding

* tests: added unit test to track progress

* chore: rebased v0.11.x

* feat: implemented cancellation

* feat: added overloaded callTool method

* fix: encoding Response _meta fix

* chore: added old version to the versions list

* feat: 2025-11-25 phase 2; added Logging and Completions (#196)

* feat: added Logging and Completions

* feat: updated EmbeddedResource for the latest spec

* chore: changed encoding of meta in Prompts to encodeIfPresent

* feat: compliance tests + http server transport (#197)

* feat: http server transport

* feat: added http server transport

* feat: removed unused files

* feat: added conformance tests

* feat: improved conformace testing

* chore: removed redundant files

* feat/2025-11-25: sampling, elicitation, roots (#198)

* feat: Roots implementation

* feat: added unit testing

* feat: improved unit tests and fixed NetworkTransport crash

* feat: implemented sampling and elicitation

* feat: improved sampling tests

* feat: improved elicitation tests

* feat: improved confirmance tests

* feat: improved Prompts unit tests

* feat: removed test code

* feat: updated conformance tests

* chore: removed redundant commas

* chore: removed 6.0.3 swift version from ci

* chore: improved unit tests a little bit

* chore: update README.md

* chore: add server progress tracking example to README.md

* chore: updated README.md

* chore: updated transports table formatting

* chore: updated MockNetworkConnection

---------

Co-authored-by: Austin Evans <austinjevans@me.com>
Co-authored-by: yehorsobko <yehorsobko@macpaw.com>
2026-02-19 13:09:31 +02:00

665 lines
22 KiB
Swift

import Foundation
import Logging
import Testing
@testable import MCP
#if canImport(Network)
import Network
/// A mock implementation of NetworkConnectionProtocol for testing
final class MockNetworkConnection: NetworkConnectionProtocol, @unchecked Sendable {
/// Current state of the connection
private var mockState: NWConnection.State = .setup
/// Error to be returned on send/receive operations
private var mockError: Swift.Error?
/// Data queue for testing
private var dataToReceive: [Data] = []
private var sentData: [Data] = []
/// The state update handler
public var stateUpdateHandler: ((@Sendable (NWConnection.State) -> Void))?
/// Current state
var state: NWConnection.State {
return mockState
}
/// Initialize a mock connection
init() {}
/// Start the connection
func start(queue: DispatchQueue) {
// Simulate successful connection by default
Task { @MainActor in
if mockError != nil {
self.updateState(mockState)
} else {
self.updateState(.ready)
}
}
}
/// Send data through the connection
func send(
content: Data?,
contentContext: NWConnection.ContentContext,
isComplete: Bool,
completion: NWConnection.SendCompletion
) {
if let content = content {
sentData.append(content)
}
switch completion {
case .contentProcessed(let handler):
Task { @MainActor in
handler(self.mockError as? NWError)
}
default:
break
}
}
/// Receive data from the connection
func receive(
minimumIncompleteLength: Int,
maximumLength: Int,
completion: @escaping @Sendable (
Data?, NWConnection.ContentContext?, Bool, NWError?
) -> Void
) {
Task { @MainActor in
if self.mockState == .cancelled {
completion(
nil, nil, true,
NWError.posix(POSIXErrorCode.ECANCELED)
)
return
}
if let error = self.mockError {
completion(nil, nil, false, error as? NWError)
return
}
if self.dataToReceive.isEmpty {
completion(Data(), nil, false, nil)
return
}
let data = self.dataToReceive.removeFirst()
completion(data, nil, self.dataToReceive.isEmpty, nil)
}
}
/// Cancel the connection
func cancel() {
updateState(.cancelled)
}
// Test helpers
/// Simulate the connection becoming ready
func simulateReady() {
updateState(.ready)
}
/// Simulate the connection becoming preparing
func simulatePreparing() {
updateState(.preparing)
}
/// Simulate a connection failure
func simulateFailure(
error: Swift.Error? = nil
) {
mockError = error
if let nwError = error as? NWError {
updateState(.failed(nwError))
} else {
updateState(.failed(NWError.posix(POSIXErrorCode(rawValue: 57)!)))
}
}
/// Simulate connection cancellation
func simulateCancellation() {
updateState(.cancelled)
}
/// Update the connection state and notify handler
private func updateState(_ newState: NWConnection.State) {
mockState = newState
Task { @MainActor in
self.stateUpdateHandler?(newState)
}
}
/// Queue data to be received
func queueDataForReceiving(_ data: Data) {
dataToReceive.append(data)
}
/// Queue a heartbeat message to be received
func queueHeartbeat() {
// Create a mock heartbeat message that matches the format
let magicBytes: [UInt8] = [0xF0, 0x9F, 0x92, 0x93] // Magic bytes for heartbeat
var data = Data(magicBytes)
let timestamp = UInt64(Date().timeIntervalSinceReferenceDate * 1000)
withUnsafeBytes(of: timestamp) { buffer in
data.append(contentsOf: buffer)
}
queueDataForReceiving(data)
}
/// Queue text message to be received
func queueTextMessage(_ text: String) {
guard let data = text.data(using: .utf8) else { return }
queueDataForReceiving(data)
}
/// Get all sent data
func getSentData() -> [Data] {
return sentData
}
/// Clear sent data buffer
func clearSentData() {
sentData.removeAll()
}
}
@Suite("Network Transport Tests", .serialized)
struct NetworkTransportTests {
@Test("Heartbeat Creation And Parsing")
func testHeartbeatCreationAndParsing() {
// Create a heartbeat
let heartbeat = NetworkTransport.Heartbeat()
// Convert to data and back
let data = heartbeat.data
let parsed = NetworkTransport.Heartbeat.from(data: data)
#expect(parsed != nil)
// Time should be very close (within 1 second)
if let parsed = parsed {
let timeDifference = abs(parsed.timestamp.timeIntervalSince(heartbeat.timestamp))
#expect(timeDifference < 1.0)
}
// Test invalid data
let invalidData = Data([0x01, 0x02, 0x03])
#expect(NetworkTransport.Heartbeat.from(data: invalidData) == nil)
#expect(NetworkTransport.Heartbeat.isHeartbeat(invalidData) == false)
#expect(NetworkTransport.Heartbeat.isHeartbeat(data) == true)
}
@Test("Reconnection Configuration")
func testReconnectionConfiguration() {
// Create custom config
let config = NetworkTransport.ReconnectionConfiguration(
enabled: true,
maxAttempts: 3,
backoffMultiplier: 2.0
)
#expect(config.enabled == true)
#expect(config.maxAttempts == 3)
#expect(config.backoffMultiplier == 2.0)
// Test backoff delay calculation
let firstDelay = config.backoffDelay(for: 1)
let secondDelay = config.backoffDelay(for: 2)
let thirdDelay = config.backoffDelay(for: 3)
// Check delays are approximately correct (within 0.001)
#expect(abs(firstDelay - 0.5) < 0.001)
#expect(abs(secondDelay - 1.0) < 0.001)
#expect(abs(thirdDelay - 2.0) < 0.001)
// Test disabled config
let disabledConfig = NetworkTransport.ReconnectionConfiguration.disabled
#expect(disabledConfig.enabled == false)
}
@Test("Heartbeat Configuration")
func testHeartbeatConfiguration() {
// Create custom config
let config = NetworkTransport.HeartbeatConfiguration(
enabled: true,
interval: 5.0
)
#expect(config.enabled == true)
#expect(config.interval == 5.0)
// Test default config
let defaultConfig = NetworkTransport.HeartbeatConfiguration.default
#expect(defaultConfig.enabled == true)
#expect(defaultConfig.interval == 15.0)
// Test disabled config
let disabledConfig = NetworkTransport.HeartbeatConfiguration.disabled
#expect(disabledConfig.enabled == false)
}
@Test("Connect Success")
func testNetworkTransportConnectSuccess() async throws {
let mockConnection = MockNetworkConnection()
let transport = NetworkTransport(
mockConnection,
heartbeatConfig: .disabled // Disable heartbeats for simplified testing
)
try await transport.connect()
// Verify connection state
#expect(mockConnection.state == .ready)
await transport.disconnect()
}
@Test("Connect Failure")
func testNetworkTransportConnectFailure() async {
let mockConnection = MockNetworkConnection()
let transport = NetworkTransport(
mockConnection,
reconnectionConfig: .disabled // Disable reconnection for this test
)
// Simulate failure before connecting
mockConnection.simulateFailure(error: NWError.posix(POSIXErrorCode.ECONNRESET))
do {
try await transport.connect()
Issue.record("Expected connect to throw an error")
} catch let error as MCPError {
// Expected failure
#expect(error.localizedDescription.contains("Connection failed"))
} catch let error as NWError {
// Also accept NWError since it's the underlying error
#expect((error as NSError).code == POSIXErrorCode.ECONNRESET.rawValue)
} catch {
Issue.record("Unexpected error type: \(type(of: error))")
}
await transport.disconnect()
}
@Test("Send Message")
func testNetworkTransportSendMessage() async throws {
let mockConnection = MockNetworkConnection()
let transport = NetworkTransport(
mockConnection,
heartbeatConfig: .disabled
)
try await transport.connect()
// Test sending a simple message
let message = #"{"key":"value"}"#.data(using: .utf8)!
try await transport.send(message)
// Verify the message was sent with a newline delimiter
let sentData = mockConnection.getSentData()
#expect(sentData.count == 1)
if sentData.count > 0 {
let expectedOutput = message + "\n".data(using: .utf8)!
#expect(sentData[0] == expectedOutput)
}
await transport.disconnect()
}
@Test("Receive Message")
func testNetworkTransportReceiveMessage() async throws {
let mockConnection = MockNetworkConnection()
let transport = NetworkTransport(
mockConnection,
heartbeatConfig: .disabled
)
// Queue a message to be received
let message = #"{"key":"value"}"#
let messageWithNewline = message + "\n"
mockConnection.queueTextMessage(messageWithNewline)
try await transport.connect()
// Start receiving messages
let stream = await transport.receive()
var iterator = stream.makeAsyncIterator()
// Get first message
let received = try await iterator.next()
#expect(received != nil)
if let received = received {
#expect(received == message.data(using: .utf8)!)
}
await transport.disconnect()
}
@Test("Heartbeat Send and Receive")
func testNetworkTransportHeartbeat() async throws {
let mockConnection = MockNetworkConnection()
// Create transport with rapid heartbeats
let heartbeatConfig = NetworkTransport.HeartbeatConfiguration(
enabled: true,
interval: 0.1 // Short interval for testing
)
let transport = NetworkTransport(
mockConnection,
heartbeatConfig: heartbeatConfig
)
try await transport.connect()
// Wait for initial connection setup
try await Task.sleep(for: .milliseconds(100))
// Wait for initial heartbeat delay (1 second) plus a small buffer
try await Task.sleep(for: .seconds(1.2))
// Check if heartbeat was sent
let sentData = mockConnection.getSentData()
#expect(sentData.count >= 1, "No heartbeat was sent after \(sentData.count) attempts")
if let firstSent = sentData.first {
#expect(
NetworkTransport.Heartbeat.isHeartbeat(firstSent),
"Sent data is not a heartbeat")
}
// Queue a heartbeat to be received
mockConnection.queueHeartbeat()
// Wait for heartbeat processing
try await Task.sleep(for: .milliseconds(100))
await transport.disconnect()
}
@Test("Reconnection")
func testNetworkTransportReconnection() async throws {
let mockConnection = MockNetworkConnection()
// Configure for quick reconnection
let reconnectionConfig = NetworkTransport.ReconnectionConfiguration(
enabled: true,
maxAttempts: 2,
backoffMultiplier: 1.0
)
let transport = NetworkTransport(
mockConnection,
heartbeatConfig: .disabled,
reconnectionConfig: reconnectionConfig
)
try await transport.connect()
// Simulate connection failure during operation
mockConnection.simulateFailure(error: NWError.posix(POSIXErrorCode.ECONNRESET))
// Wait a bit to ensure failure is processed
try await Task.sleep(for: .milliseconds(100))
// Try to send message after failure - should trigger reconnection process
let message = #"{"test":"reconnect"}"#.data(using: .utf8)!
do {
try await transport.send(message)
Issue.record("Expected send to fail after connection lost")
} catch {
// Expected error
#expect(error is MCPError, "Expected MCPError but got \(type(of: error))")
}
// Wait for potential reconnection attempt
try await Task.sleep(for: .milliseconds(600))
await transport.disconnect()
}
@Test("Multiple Messages")
func testNetworkTransportMultipleMessages() async throws {
let mockConnection = MockNetworkConnection()
let transport = NetworkTransport(
mockConnection,
heartbeatConfig: .disabled
)
// Queue multiple messages
let messages = [
#"{"id":1,"method":"test1"}"#,
#"{"id":2,"method":"test2"}"#,
#"{"id":3,"method":"test3"}"#,
]
for message in messages {
mockConnection.queueTextMessage(message + "\n")
}
try await transport.connect()
// Receive and verify all messages
let stream = await transport.receive()
var receiveCount = 0
for try await data in stream {
if let receivedStr = String(data: data, encoding: .utf8) {
#expect(messages.contains(receivedStr))
receiveCount += 1
if receiveCount >= messages.count {
break
}
}
}
await transport.disconnect()
}
@Test("Disconnect During Receive")
func testNetworkTransportDisconnectDuringReceive() async throws {
let mockConnection = MockNetworkConnection()
let transport = NetworkTransport(
mockConnection,
heartbeatConfig: .disabled
)
try await transport.connect()
// Start a task to receive messages
let receiveTask = Task {
var count = 0
for try await _ in await transport.receive() {
count += 1
if count > 10 {
// Prevent infinite loop in test
break
}
}
}
// Let the receive loop start
try await Task.sleep(for: .milliseconds(100))
// Disconnect while receiving
await transport.disconnect()
// Wait for the receive task to complete
_ = await receiveTask.result
}
@Test("Partial Message Reception")
func testPartialMessageReception() async throws {
let mockConnection = MockNetworkConnection()
let transport = NetworkTransport(
mockConnection,
heartbeatConfig: .disabled
)
try await transport.connect()
// Split a message into multiple parts
let message = #"{"key":"value"}"#
let parts = [
message.prefix(5).data(using: .utf8)!,
message.dropFirst(5).data(using: .utf8)!,
"\n".data(using: .utf8)!,
]
// Queue the parts
for part in parts {
mockConnection.queueDataForReceiving(part)
}
// Start receiving messages
let stream = await transport.receive()
var iterator = stream.makeAsyncIterator()
// Get the complete message
let received = try await iterator.next()
#expect(received != nil)
if let received = received {
#expect(received == message.data(using: .utf8)!)
}
await transport.disconnect()
}
@Test("Large Message Handling")
func testLargeMessageHandling() async throws {
let mockConnection = MockNetworkConnection()
let transport = NetworkTransport(
mockConnection,
heartbeatConfig: .disabled
)
try await transport.connect()
// Create a large message (larger than typical receive buffer)
let largeMessage = String(repeating: "x", count: 100_000)
let messageWithNewline = largeMessage + "\n"
mockConnection.queueTextMessage(messageWithNewline)
// Start receiving messages
let stream = await transport.receive()
var iterator = stream.makeAsyncIterator()
// Get the message
let received = try await iterator.next()
#expect(received != nil)
if let received = received {
#expect(received.count == largeMessage.count)
}
await transport.disconnect()
}
@Test("Reconnection Backoff")
func testReconnectionBackoff() async throws {
let mockConnection = MockNetworkConnection()
let startTime = Date()
// Configure for quick reconnection with known backoff
let reconnectionConfig = NetworkTransport.ReconnectionConfiguration(
enabled: true,
maxAttempts: 3,
backoffMultiplier: 2.0
)
let transport = NetworkTransport(
mockConnection,
heartbeatConfig: .disabled,
reconnectionConfig: reconnectionConfig
)
try await transport.connect()
// Simulate failure
mockConnection.simulateFailure(error: NWError.posix(POSIXErrorCode.ECONNRESET))
// Wait for reconnection attempts
try await Task.sleep(for: .seconds(4))
let endTime = Date()
let duration = endTime.timeIntervalSince(startTime)
// Verify we had enough time for reconnection attempts
#expect(duration >= 3.5) // Should have time for 3 attempts with backoff
await transport.disconnect()
}
@Test("Heartbeat Failure Handling")
func testHeartbeatFailureHandling() async throws {
let mockConnection = MockNetworkConnection()
// Create transport with rapid heartbeats
let heartbeatConfig = NetworkTransport.HeartbeatConfiguration(
enabled: true,
interval: 0.1
)
let transport = NetworkTransport(
mockConnection,
heartbeatConfig: heartbeatConfig
)
try await transport.connect()
// Wait for initial heartbeat
try await Task.sleep(for: .seconds(1.2))
// Simulate failure during heartbeat
mockConnection.simulateFailure(error: NWError.posix(POSIXErrorCode.ECONNRESET))
// Wait for potential recovery
try await Task.sleep(for: .milliseconds(500))
// Verify connection state
if case .failed = mockConnection.state {
// expected
} else {
Issue.record("Expected state to be failed")
}
await transport.disconnect()
}
@Test("Resource Cleanup")
func testResourceCleanup() async throws {
weak var weakConnection: MockNetworkConnection?
do {
let mockConnection = MockNetworkConnection()
weakConnection = mockConnection
// Create and use transport in a separate scope
let transport = NetworkTransport(
mockConnection,
heartbeatConfig: .disabled
)
try await transport.connect()
await transport.disconnect()
}
// Wait for potential async cleanup
try await Task.sleep(for: .milliseconds(100))
// Verify connection is cleaned up
#expect(weakConnection == nil, "Connection was not properly cleaned up")
}
}
#endif