From aff5d599eb038ee0b791196a30756c019100c617 Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Wed, 11 Feb 2026 11:37:36 +0000 Subject: [PATCH 01/13] Refactor testing utilities --- .../NIOHTTPServerEndToEndTests.swift | 12 +- .../NIOHTTPServerTests.swift | 70 +++++++--- .../NIOHTTPServerTests/Utilities/Client.swift | 101 -------------- .../Utilities/Helpers.swift | 39 ++++++ .../Utilities/NIOClient/NIOClient+HTTP1.swift | 51 +++++++ .../NIOClient/NIOClient+SecureUpgrade.swift | 125 ++++++++++++++++++ .../NegotiatedClientConnection.swift | 63 +++++++++ .../NIOHTTPServer+HTTP1.swift | 2 + .../NIOHTTPServer+SecureUpgrade.swift | 2 + .../TestingClientServerProvider+HTTP1.swift} | 76 ++++++----- ...gClientServerProvider+SecureUpgrade.swift} | 90 ++++--------- 11 files changed, 402 insertions(+), 229 deletions(-) delete mode 100644 Tests/NIOHTTPServerTests/Utilities/Client.swift create mode 100644 Tests/NIOHTTPServerTests/Utilities/Helpers.swift create mode 100644 Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+HTTP1.swift create mode 100644 Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+SecureUpgrade.swift create mode 100644 Tests/NIOHTTPServerTests/Utilities/NegotiatedClientConnection.swift rename Tests/NIOHTTPServerTests/Utilities/{ => TestingChannelClientServer}/NIOHTTPServer+HTTP1.swift (92%) rename Tests/NIOHTTPServerTests/Utilities/{ => TestingChannelClientServer}/NIOHTTPServer+SecureUpgrade.swift (91%) rename Tests/NIOHTTPServerTests/Utilities/{HTTP1ClientServerProvider.swift => TestingChannelClientServer/TestingClientServerProvider+HTTP1.swift} (65%) rename Tests/NIOHTTPServerTests/Utilities/{HTTPSecureUpgradeClientServerProvider.swift => TestingChannelClientServer/TestingClientServerProvider+SecureUpgrade.swift} (64%) diff --git a/Tests/NIOHTTPServerTests/NIOHTTPServerEndToEndTests.swift b/Tests/NIOHTTPServerTests/NIOHTTPServerEndToEndTests.swift index f134252..fabf397 100644 --- a/Tests/NIOHTTPServerTests/NIOHTTPServerEndToEndTests.swift +++ b/Tests/NIOHTTPServerTests/NIOHTTPServerEndToEndTests.swift @@ -14,6 +14,7 @@ import HTTPServer import HTTPTypes +import Logging import NIOCore import NIOEmbedded import NIOHTTP1 @@ -31,8 +32,9 @@ struct NIOHTTPServerEndToEndTests { @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) @Test("HTTP/1.1 request and response") func testHTTP1_1() async throws { - try await HTTP1ClientServerProvider.withProvider( - handler: HTTPServerClosureRequestHandler { request, reqContext, reqReader, resSender in + try await HTTP1TestingChannelClientServerProvider.withProvider( + logger: Logger(label: "NIOHTTPServerEndToEndTests"), + serverRequestHandler: HTTPServerClosureRequestHandler { request, reqContext, reqReader, resSender in let sender = try await resSender.send(.init(status: .ok)) try await sender.produceAndConclude { writer in @@ -42,7 +44,7 @@ struct NIOHTTPServerEndToEndTests { } } ) { clientServerProvider in - try await clientServerProvider.withConnectedClient { client in + try await clientServerProvider.withConnection { client in try await client.executeThenClose { inbound, outbound in try await outbound.write(.head(.init(method: .get, scheme: "", authority: "", path: "/"))) try await outbound.write(.end(nil)) @@ -91,6 +93,7 @@ struct NIOHTTPServerEndToEndTests { clientTLSConfig.applicationProtocols = ["h2"] try await HTTPSecureUpgradeClientServerProvider.withProvider( + logger: Logger(label: "NIOHTTPServerEndToEndTests"), tlsConfiguration: serverTLSConfig, handler: HTTPServerClosureRequestHandler { request, reqContext, reqReader, resSender in let sender = try await resSender.send(.init(status: .ok)) @@ -102,11 +105,12 @@ struct NIOHTTPServerEndToEndTests { } } ) { clientServerProvider in - try await clientServerProvider.withConnectedClient(clientTLSConfiguration: clientTLSConfig) { + try await clientServerProvider.withConnectedClient(tlsConfig: clientTLSConfig) { negotiatedConnection in switch negotiatedConnection { case .http1(_): Issue.record("Failed to negotiate HTTP/2 despite the client requiring HTTP/2.") + case .http2(let http2StreamManager): let http2AsyncChannel = try await http2StreamManager.openStream() diff --git a/Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift b/Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift index f001d02..3327c59 100644 --- a/Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift +++ b/Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift @@ -53,7 +53,7 @@ struct NIOHTTPServerTests { @Test("Obtain the listening address correctly") func testListeningAddress() async throws { let server = NIOHTTPServer( - logger: Logger(label: "Test"), + logger: Logger(label: "NIOHTTPServerTests"), configuration: .init(bindTarget: .hostAndPort(host: "127.0.0.1", port: 1234)) ) @@ -81,7 +81,7 @@ struct NIOHTTPServerTests { @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) func testPlaintext() async throws { let server = NIOHTTPServer( - logger: Logger(label: "Test"), + logger: Logger(label: "NIOHTTPServerTests"), configuration: .init(bindTarget: .hostAndPort(host: "127.0.0.1", port: 0)) ) @@ -112,7 +112,7 @@ struct NIOHTTPServerTests { let serverAddress = try await server.listeningAddress - let client = try await setUpClient(host: serverAddress.host, port: serverAddress.port) + let client = try await NIOHTTP1Client.setUpClient(at: serverAddress) try await client.executeThenClose { inbound, outbound in try await outbound.write(Self.reqHead) try await outbound.write(Self.reqBody) @@ -143,7 +143,7 @@ struct NIOHTTPServerTests { let clientChain = try TestCA.makeSelfSignedChain() let server = NIOHTTPServer( - logger: Logger(label: "Test"), + logger: Logger(label: "NIOHTTPServerTests"), configuration: .init( bindTarget: .hostAndPort(host: "127.0.0.1", port: 0), transportSecurity: .mTLS( @@ -193,29 +193,61 @@ struct NIOHTTPServerTests { let serverAddress = try await server.listeningAddress - let clientChannel = try await setUpClientWithMTLS( + let clientChannel = try await NIOSecureUpgradeClient.setUpMTLSClient( at: serverAddress, - chain: clientChain, + clientChain: clientChain, trustRoots: [serverChain.ca], applicationProtocol: applicationProtocol ) - try await clientChannel.executeThenClose { inbound, outbound in - try await outbound.write(Self.reqHead) - try await outbound.write(Self.reqBody) - try await outbound.write(Self.reqEnd) + switch clientChannel { + case .http1(let client): + guard applicationProtocol == "http/1.1" else { + Issue.record("Unexpectedly negotiated a HTTP/1.1 connection") + return + } - for try await response in inbound { - try await Self.clientResponseHandler( - response, - expectedStatus: .ok, - expectedBody: .init([1, 2]), - expectedTrailers: Self.trailer - ) + try await client.executeThenClose { inbound, outbound in + try await outbound.write(Self.reqHead) + try await outbound.write(Self.reqBody) + try await outbound.write(Self.reqEnd) + + for try await response in inbound { + try await Self.clientResponseHandler( + response, + expectedStatus: .ok, + expectedBody: .init([1, 2]), + expectedTrailers: Self.trailer + ) + } + } + // Cancel the server and client task once we know the client has received the response + group.cancelAll() + + case .http2(let streamManager): + guard applicationProtocol == "h2" else { + Issue.record("Unexpectedly negotiated a HTTP/2 connection") + return } + + let streamChannel = try await streamManager.openStream() + try await streamChannel.executeThenClose { inbound, outbound in + try await outbound.write(Self.reqHead) + try await outbound.write(Self.reqBody) + try await outbound.write(Self.reqEnd) + + for try await response in inbound { + try await Self.clientResponseHandler( + response, + expectedStatus: .ok, + expectedBody: .init([1, 2]), + expectedTrailers: Self.trailer + ) + } + } + // Cancel the server and client task once we know the client has received the response + group.cancelAll() } - // Cancel the server and client task once we know the client has received the response - group.cancelAll() } } } diff --git a/Tests/NIOHTTPServerTests/Utilities/Client.swift b/Tests/NIOHTTPServerTests/Utilities/Client.swift deleted file mode 100644 index 0428875..0000000 --- a/Tests/NIOHTTPServerTests/Utilities/Client.swift +++ /dev/null @@ -1,101 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP Server open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift HTTP Server project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import NIOCore -import NIOHTTPTypes -import NIOHTTPTypesHTTP1 -import NIOHTTPTypesHTTP2 -import NIOPosix -import NIOSSL -import X509 - -@testable import NIOHTTPServer - -@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) -func setUpClient(host: String, port: Int) async throws -> NIOAsyncChannel { - try await ClientBootstrap(group: .singletonMultiThreadedEventLoopGroup) - .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) - .connect(to: try .init(ipAddress: host, port: port)) { channel in - channel.eventLoop.makeCompletedFuture { - try channel.pipeline.syncOperations.addHTTPClientHandlers() - try channel.pipeline.syncOperations.addHandler(HTTP1ToHTTPClientCodec()) - - return try NIOAsyncChannel( - wrappingChannelSynchronously: channel, - configuration: .init() - ) - } - } -} - -@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) -func setUpClientWithMTLS( - at address: NIOHTTPServer.SocketAddress, - chain: ChainPrivateKeyPair, - trustRoots: [Certificate], - applicationProtocol: String, -) async throws -> NIOAsyncChannel { - var clientTLSConfig = TLSConfiguration.makeClientConfiguration() - clientTLSConfig.certificateChain = [try NIOSSLCertificateSource(chain.leaf)] - clientTLSConfig.privateKey = .privateKey(try .init(chain.privateKey)) - clientTLSConfig.trustRoots = .certificates(try trustRoots.map { try NIOSSLCertificate($0) }) - clientTLSConfig.certificateVerification = .noHostnameVerification - clientTLSConfig.applicationProtocols = [applicationProtocol] - - let sslContext = try NIOSSLContext(configuration: clientTLSConfig) - - let clientNegotiatedChannel = try await ClientBootstrap(group: .singletonMultiThreadedEventLoopGroup) - .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) - .connect(to: try .init(ipAddress: address.host, port: address.port)) { channel in - channel.eventLoop.makeCompletedFuture { - let sslHandler = try NIOSSLClientHandler(context: sslContext, serverHostname: nil) - try channel.pipeline.syncOperations.addHandler(sslHandler) - }.flatMap { - channel.configureHTTP2AsyncSecureUpgrade( - http1ConnectionInitializer: { channel in - channel.eventLoop.makeCompletedFuture { - try channel.pipeline.syncOperations.addHTTPClientHandlers() - try channel.pipeline.syncOperations.addHandlers(HTTP1ToHTTPClientCodec()) - - return try NIOAsyncChannel( - wrappingChannelSynchronously: channel, - configuration: .init() - ) - } - }, - http2ConnectionInitializer: { channel in - channel.configureAsyncHTTP2Pipeline(mode: .client) { $0.eventLoop.makeSucceededFuture($0) } - } - ) - } - }.get() - - switch clientNegotiatedChannel { - case .http1_1(let http1Channel): - precondition(applicationProtocol == "http/1.1", "Unexpectedly established a HTTP 1.1 channel") - return http1Channel - - case .http2(let http2Channel): - precondition(applicationProtocol == "h2", "Unexpectedly established a HTTP 2 channel") - return try await http2Channel.openStream { channel in - channel.eventLoop.makeCompletedFuture { - try channel.pipeline.syncOperations.addHandler(HTTP2FramePayloadToHTTPClientCodec()) - return try NIOAsyncChannel( - wrappingChannelSynchronously: channel, - configuration: .init() - ) - } - } - } -} diff --git a/Tests/NIOHTTPServerTests/Utilities/Helpers.swift b/Tests/NIOHTTPServerTests/Utilities/Helpers.swift new file mode 100644 index 0000000..d973368 --- /dev/null +++ b/Tests/NIOHTTPServerTests/Utilities/Helpers.swift @@ -0,0 +1,39 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP Server open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore +import NIOEmbedded + +extension NIOAsyncTestingChannel { + /// Forwards all of our outbound writes to `other` and vice-versa. + func glueTo(_ other: NIOAsyncTestingChannel) async throws { + await withThrowingTaskGroup { group in + // 1. Forward all `self` writes to `other` + group.addTask { + while !Task.isCancelled { + let ourPart = try await self.waitForOutboundWrite(as: ByteBuffer.self) + try await other.writeInbound(ourPart) + } + } + + // 2. Forward all `other` writes to `self` + group.addTask { + while !Task.isCancelled { + let otherPart = try await other.waitForOutboundWrite(as: ByteBuffer.self) + try await self.writeInbound(otherPart) + } + } + } + } +} diff --git a/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+HTTP1.swift b/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+HTTP1.swift new file mode 100644 index 0000000..72843e3 --- /dev/null +++ b/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+HTTP1.swift @@ -0,0 +1,51 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP Server open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift HTTP Server project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore +import NIOHTTPServer +import NIOHTTPTypes +import NIOHTTPTypesHTTP1 +import NIOPosix + +/// Provides NIO HTTP clients for testing that one can interact with using `NIOHTTPTypes`. +/// +/// Server responses are `NIOHTTPTypes/HTTPResponsePart` and client requests are `NIOHTTPTypes/HTTPRequestPart`. +/// With the ``NIOAsyncChannel`` the `setUpClient` methods vend, one can write `HTTPRequestPart`s to the channel +/// and observe `HTTPResponsePart`s from the inbound stream of the async channel. +@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) +struct NIOHTTP1Client { + /// Configures a channel with HTTP/1.1 client handlers for interaction in terms of HTTP types. + static func clientChannelInitializer(_ channel: Channel) throws { + try channel.pipeline.syncOperations.addHTTPClientHandlers() + try channel.pipeline.syncOperations.addHandler(HTTP1ToHTTPClientCodec()) + } + + /// Creates and connects an HTTP/1.1 client to the specified address. + static func setUpClient( + at address: NIOHTTPServer.SocketAddress + ) async throws -> NIOAsyncChannel { + try await ClientBootstrap(group: .singletonMultiThreadedEventLoopGroup) + .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) + .connect(to: try .init(ipAddress: address.host, port: address.port)) { channel in + channel.eventLoop.makeCompletedFuture { + try self.clientChannelInitializer(channel) + + return try NIOAsyncChannel( + wrappingChannelSynchronously: channel, + configuration: .init(isOutboundHalfClosureEnabled: true) + ) + } + } + } +} diff --git a/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+SecureUpgrade.swift b/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+SecureUpgrade.swift new file mode 100644 index 0000000..e50ae60 --- /dev/null +++ b/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+SecureUpgrade.swift @@ -0,0 +1,125 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP Server open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore +import NIOHTTP2 +import NIOHTTPTypes +import NIOPosix +import NIOSSL +import X509 + +@testable import NIOHTTPServer + +@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) +/// Provides a HTTP client with ALPN negotiation. +struct NIOSecureUpgradeClient { + /// Valid `applicationProtocol` values are `"http/1.1"` (forces HTTP/1.1), `"h2"` (forces HTTP/2), or a + /// comma-separated combination of both in order of preference, e.g. `"http/1.1, h2"`. + static func setUpTLSConfig(trustRoots: [Certificate], applicationProtocol: String) throws -> TLSConfiguration { + var clientTLSConfig = TLSConfiguration.makeClientConfiguration() + clientTLSConfig.trustRoots = .certificates(try trustRoots.map { try NIOSSLCertificate($0) }) + clientTLSConfig.certificateVerification = .noHostnameVerification + clientTLSConfig.applicationProtocols = [applicationProtocol] + + return clientTLSConfig + } + + /// Creates and connects a TLS-enabled client to the specified address with ALPN negotiation. + static func setUpClient( + at address: NIOHTTPServer.SocketAddress, + trustRoots: [Certificate], + applicationProtocol: String + ) async throws -> NegotiatedClientConnection { + let tlsConfig = try self.setUpTLSConfig(trustRoots: trustRoots, applicationProtocol: applicationProtocol) + + return try await self._setUpClient(at: address, tlsConfig: tlsConfig) + } + + /// Exactly like ``setUpClient(at:trustRoots:applicationProtocol:)`` but with mTLS enabled. + static func setUpMTLSClient( + at address: NIOHTTPServer.SocketAddress, + clientChain: ChainPrivateKeyPair, + trustRoots: [Certificate], + applicationProtocol: String, + ) async throws -> NegotiatedClientConnection { + var tlsConfig = try self.setUpTLSConfig(trustRoots: trustRoots, applicationProtocol: applicationProtocol) + tlsConfig.certificateChain = [try NIOSSLCertificateSource(clientChain.leaf)] + tlsConfig.privateKey = .privateKey(try .init(clientChain.privateKey)) + + return try await self._setUpClient(at: address, tlsConfig: tlsConfig) + } + + /// Creates and connects a client to the specified address with the provided TLS configuration. + private static func _setUpClient( + at address: NIOHTTPServer.SocketAddress, + tlsConfig: TLSConfiguration + ) async throws -> NegotiatedClientConnection { + let clientNegotiatedChannel = try await ClientBootstrap(group: .singletonMultiThreadedEventLoopGroup) + .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) + .connect(to: try .init(ipAddress: address.host, port: address.port)) { channel in + channel.eventLoop.makeCompletedFuture { + try self.sslClientChannelInitializer(channel, tlsConfig: tlsConfig) + }.flatMap { + self.clientChannelInitializer(channel, tlsConfig: tlsConfig) + } + }.get() + + switch clientNegotiatedChannel { + case .http1_1(let http1Channel): + return .http1(http1Channel) + + case .http2(let http2Channel): + return .http2(.init(http2StreamMultiplexer: http2Channel)) + } + } + + /// Sets up the input child channel with a ``NIOSSLClientHandler`` configured with the provided TLS + /// configuration. + static func sslClientChannelInitializer(_ channel: Channel, tlsConfig: TLSConfiguration) throws { + let sslContext = try NIOSSLContext(configuration: tlsConfig) + + let sslHandler = try NIOSSLClientHandler(context: sslContext, serverHostname: nil) + try channel.pipeline.syncOperations.addHandler(sslHandler) + } + + /// Provides channel initializers for HTTP/1.1 and HTTP/2 to the ALPN handler, which selects one based on + /// the negotiated result. + static func clientChannelInitializer( + _ channel: Channel, + tlsConfig: TLSConfiguration + ) -> EventLoopFuture< + EventLoopFuture< + NIONegotiatedHTTPVersion< + NIOAsyncChannel, + NIOHTTP2Handler.AsyncStreamMultiplexer + > + > + > { + channel.configureHTTP2AsyncSecureUpgrade( + http1ConnectionInitializer: { channel in + channel.eventLoop.makeCompletedFuture { + try NIOHTTP1Client.clientChannelInitializer(channel) + + return try NIOAsyncChannel( + wrappingChannelSynchronously: channel, + configuration: .init(isOutboundHalfClosureEnabled: true) + ) + } + }, + http2ConnectionInitializer: { channel in + channel.configureAsyncHTTP2Pipeline(mode: .client) { $0.eventLoop.makeSucceededFuture($0) } + } + ) + } +} diff --git a/Tests/NIOHTTPServerTests/Utilities/NegotiatedClientConnection.swift b/Tests/NIOHTTPServerTests/Utilities/NegotiatedClientConnection.swift new file mode 100644 index 0000000..5292c6c --- /dev/null +++ b/Tests/NIOHTTPServerTests/Utilities/NegotiatedClientConnection.swift @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP Server open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore +import NIOHTTP2 +import NIOHTTPTypes +import NIOHTTPTypesHTTP2 + +/// A testing utility that wraps the result of ALPN negotiation for HTTP/1.1 or HTTP/2 client connections. +/// +/// - If HTTP/1.1 is negotiated, this type vends the underlying client connection channel. +/// - If HTTP/2 is negotiated, this type vends a ``HTTP2StreamManager``. In tests, you can then call the +/// ``HTTP2StreamManager/openStream()`` method, which will create a stream channel, set it up with a channel handler, +/// and return a ``NIOAsyncChannel`` from which you can send/observe requests/responses in terms of HTTP types. +enum NegotiatedClientConnection { + case http1(NIOAsyncChannel) + case http2(HTTP2StreamManager) + + init( + negotiationResult: NIONegotiatedHTTPVersion< + NIOAsyncChannel, NIOHTTP2Handler.AsyncStreamMultiplexer + > + ) async throws { + switch negotiationResult { + case .http1_1(let http1AsyncChannel): + self = .http1(http1AsyncChannel) + + case .http2(let http2StreamMultiplexer): + self = .http2(.init(http2StreamMultiplexer: http2StreamMultiplexer)) + } + } + + /// Provides utilities for managing HTTP/2 streams. + struct HTTP2StreamManager { + let http2StreamMultiplexer: NIOHTTP2Handler.AsyncStreamMultiplexer + + /// A wrapper over `NIOHTTP2Handler/AsyncStreamMultiplexer/openStream(_:)` that first initializes the stream + /// channel with the `HTTP2FramePayloadToHTTPClientCodec` channel handler, and wraps it in a `NIOAsyncChannel` + /// (with outbound half closure enabled). + func openStream() async throws -> NIOAsyncChannel { + try await self.http2StreamMultiplexer.openStream { channel in + channel.eventLoop.makeCompletedFuture { + try channel.pipeline.syncOperations.addHandler(HTTP2FramePayloadToHTTPClientCodec()) + return try NIOAsyncChannel( + wrappingChannelSynchronously: channel, + configuration: .init(isOutboundHalfClosureEnabled: true) + ) + } + } + } + } +} diff --git a/Tests/NIOHTTPServerTests/Utilities/NIOHTTPServer+HTTP1.swift b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/NIOHTTPServer+HTTP1.swift similarity index 92% rename from Tests/NIOHTTPServerTests/Utilities/NIOHTTPServer+HTTP1.swift rename to Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/NIOHTTPServer+HTTP1.swift index 0feea28..195e1df 100644 --- a/Tests/NIOHTTPServerTests/Utilities/NIOHTTPServer+HTTP1.swift +++ b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/NIOHTTPServer+HTTP1.swift @@ -21,6 +21,8 @@ import NIOHTTPTypes @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) extension NIOHTTPServer { + /// Starts serving plaintext HTTP/1.1 using the provided testing channel instead of using `ServerBootstrap` as + /// `NIOHTTPServer` normally does. func serveInsecureHTTP1_1WithTestChannel( testChannel: NIOAsyncTestingChannel, handler: some HTTPServerRequestHandler diff --git a/Tests/NIOHTTPServerTests/Utilities/NIOHTTPServer+SecureUpgrade.swift b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/NIOHTTPServer+SecureUpgrade.swift similarity index 91% rename from Tests/NIOHTTPServerTests/Utilities/NIOHTTPServer+SecureUpgrade.swift rename to Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/NIOHTTPServer+SecureUpgrade.swift index e93d08f..fce2cc1 100644 --- a/Tests/NIOHTTPServerTests/Utilities/NIOHTTPServer+SecureUpgrade.swift +++ b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/NIOHTTPServer+SecureUpgrade.swift @@ -21,6 +21,8 @@ import NIOHTTPTypes @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) extension NIOHTTPServer { + /// Starts serving with the Secure Upgrade transport using the provided testing channel instead of using + /// `ServerBootstrap` as `NIOHTTPServer` normally does. func serveSecureUpgradeWithTestChannel( testChannel: NIOAsyncTestingChannel, handler: some HTTPServerRequestHandler diff --git a/Tests/NIOHTTPServerTests/Utilities/HTTP1ClientServerProvider.swift b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+HTTP1.swift similarity index 65% rename from Tests/NIOHTTPServerTests/Utilities/HTTP1ClientServerProvider.swift rename to Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+HTTP1.swift index 7c1b39d..9f2e3da 100644 --- a/Tests/NIOHTTPServerTests/Utilities/HTTP1ClientServerProvider.swift +++ b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+HTTP1.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import HTTPServer +import Logging import NIOCore import NIOEmbedded import NIOHTTP1 @@ -19,20 +21,37 @@ import NIOHTTPTypes import NIOHTTPTypesHTTP1 import X509 -@testable import HTTPServer @testable import NIOHTTPServer +/// A testing utility that sets up a `NIOHTTPServer` instance (with the HTTP/1.1 transport) based on +/// `NIOAsyncTestingChannel` (instead of the `ServerSocketChannel` that `NIOHTTPServer` normally uses) and vends a +/// client instance for sending requests and observing responses. +/// +/// This provider creates a `NIOHTTPServer` instance using a `NIOAsyncTestingChannel` as its listening channel. Since no +/// network socket is actually listening for incoming connections, client connections are simulated by *writing* a +/// connection channel to the server channel. This connection channel is set up with the same handlers that +/// `ServerBootstrap` would set up and vend to `NIOHTTPServer` on an incoming connection. +/// +/// This provider vends a NIO HTTP client channel (also backed by a `NIOAsyncTestingChannel`) that can be used to send +/// requests and observer response in terms of HTTP types (`HTTPRequestPart` and `HTTPResponsePart`) to the server +/// connection channel. @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) -struct HTTP1ClientServerProvider { +struct HTTP1TestingChannelClientServerProvider { let server: NIOHTTPServer let serverTestChannel: NIOAsyncTestingChannel + /// Sets up the server with a testing channel and the provided request handler, starts the server, and provides + /// `Self` to the `body` closure. Call `withConnection(_:)` on the provided instance to simulate incoming + /// connections. static func withProvider( - handler: some HTTPServerRequestHandler, - body: (HTTP1ClientServerProvider) async throws -> Void + logger: Logger, + serverRequestHandler: some HTTPServerRequestHandler< + HTTPRequestConcludingAsyncReader, HTTPResponseConcludingAsyncWriter + >, + body: (Self) async throws -> Void ) async throws { let server = NIOHTTPServer( - logger: .init(label: "test"), + logger: logger, // The server won't actually be bound to this host and port; we just have to pass this argument configuration: .init(bindTarget: .hostAndPort(host: "127.0.0.1", port: 8000)) ) @@ -42,26 +61,27 @@ struct HTTP1ClientServerProvider { try await withThrowingTaskGroup { group in // We are ready now. Start the server with the test channel. group.addTask { - try await server.serveInsecureHTTP1_1WithTestChannel(testChannel: serverTestChannel, handler: handler) + try await server.serveInsecureHTTP1_1WithTestChannel( + testChannel: serverTestChannel, + handler: serverRequestHandler + ) } // Execute the provided closure with a `HTTP1ClientServerProvider` instance created from the server // instance and the test channel instance - try await body( - HTTP1ClientServerProvider(server: server, serverTestChannel: serverTestChannel) - ) + try await body(Self(server: server, serverTestChannel: serverTestChannel)) group.cancelAll() } } /// Starts a new connection with the server and executes the provided `body` closure. - /// - Parameter body: A closure that should send a request using the provided client instance and validate - /// the received response. - func withConnectedClient( + /// - Parameter body: A closure that should send a request using the provided client instance and validate the + /// received response. + func withConnection( body: (NIOAsyncChannel) async throws -> Void ) async throws { - // Create a test connection channel + // Create a connection channel that we will write to the server. let serverTestConnectionChannel = NIOAsyncTestingChannel() let connectionPromise = serverTestConnectionChannel.eventLoop.makePromise(of: Void.self) @@ -95,12 +115,13 @@ struct HTTP1ClientServerProvider { } } + /// Creates a client testing channel configured with HTTP channel handlers and wraps it in a `NIOAsyncChannel`. private func setUpClientConnection() async throws -> ( - NIOAsyncTestingChannel, NIOAsyncChannel + NIOAsyncTestingChannel, + NIOAsyncChannel ) { let clientTestChannel = try await NIOAsyncTestingChannel { channel in - try channel.pipeline.syncOperations.addHTTPClientHandlers() - try channel.pipeline.syncOperations.addHandler(HTTP1ToHTTPClientCodec()) + try NIOHTTP1Client.clientChannelInitializer(channel) } // Wrap the client channel in a NIOAsyncChannel for convenience @@ -114,26 +135,3 @@ struct HTTP1ClientServerProvider { return (clientTestChannel, clientAsyncChannel) } } - -extension NIOAsyncTestingChannel { - /// Forwards all of our outbound writes to `other` and vice-versa. - func glueTo(_ other: NIOAsyncTestingChannel) async throws { - await withThrowingTaskGroup { group in - // 1. Forward all `self` writes to `other` - group.addTask { - while !Task.isCancelled { - let ourPart = try await self.waitForOutboundWrite(as: ByteBuffer.self) - try await other.writeInbound(ourPart) - } - } - - // 2. Forward all `other` writes to `self` - group.addTask { - while !Task.isCancelled { - let otherPart = try await other.waitForOutboundWrite(as: ByteBuffer.self) - try await self.writeInbound(otherPart) - } - } - } - } -} diff --git a/Tests/NIOHTTPServerTests/Utilities/HTTPSecureUpgradeClientServerProvider.swift b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+SecureUpgrade.swift similarity index 64% rename from Tests/NIOHTTPServerTests/Utilities/HTTPSecureUpgradeClientServerProvider.swift rename to Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+SecureUpgrade.swift index 41cab29..332767e 100644 --- a/Tests/NIOHTTPServerTests/Utilities/HTTPSecureUpgradeClientServerProvider.swift +++ b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+SecureUpgrade.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import HTTPServer +import Logging import NIOCore import NIOEmbedded import NIOHTTP2 @@ -21,9 +23,13 @@ import NIOHTTPTypesHTTP2 import NIOSSL import X509 -@testable import HTTPServer @testable import NIOHTTPServer +/// A testing utility that sets up a `NIOHTTPServer` instance (with the secure upgrade transport) based on +/// `NIOAsyncTestingChannel` (instead of the `ServerSocketChannel` that `NIOHTTPServer` normally uses) and vends a +/// client instance for sending requests and observing responses. +/// +/// Like ``HTTP1ClientServerProvider``, but for the secure upgrade transport. @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) struct HTTPSecureUpgradeClientServerProvider { let server: NIOHTTPServer @@ -34,7 +40,11 @@ struct HTTPSecureUpgradeClientServerProvider { let http2Configuration: NIOHTTP2Handler.Configuration + /// Sets up the server with a testing channel and the provided request handler, starts the server, and provides + /// `Self` to the `body` closure. Call `withConnectedClient(clientTLSConfiguration:body:)` on the provided instance + /// to simulate incoming connections. static func withProvider( + logger: Logger, tlsConfiguration: TLSConfiguration, tlsVerificationCallback: (@Sendable ([Certificate]) async throws -> CertificateVerificationResult)? = nil, http2Configuration: NIOHTTP2Handler.Configuration = .init(), @@ -42,7 +52,7 @@ struct HTTPSecureUpgradeClientServerProvider { body: (HTTPSecureUpgradeClientServerProvider) async throws -> Void ) async throws { let server = NIOHTTPServer( - logger: .init(label: "test"), + logger: logger, // The server won't actually be bound to this host and port; we just have to pass this argument configuration: .init(bindTarget: .hostAndPort(host: "127.0.0.1", port: 8000)) ) @@ -71,9 +81,11 @@ struct HTTPSecureUpgradeClientServerProvider { } } + /// Starts a new TLS connection with ALPN negotiation to the server and executes the provided `body` closure with + /// the negotiated ALPN result as an argument. func withConnectedClient( - clientTLSConfiguration: TLSConfiguration, - body: (NegotiatedConnection) async throws -> Void + tlsConfig: TLSConfiguration, + body: (NegotiatedClientConnection) async throws -> Void ) async throws { // Create a test connection channel let serverTestConnectionChannel = NIOAsyncTestingChannel() @@ -99,11 +111,11 @@ struct HTTPSecureUpgradeClientServerProvider { // Write the connection channel to the server channel to simulate an incoming connection try await self.serverTestChannel.writeInbound(negotiatedServerConnectionFuture) - // Now we could write requests directly to the server channle, but it expects `ByteBuffer` inputs. This is + // Now we could write requests directly to the server channel, but it expects `ByteBuffer` inputs. This is // cumbersome to work with in tests. // So, we create a client channel, and use it to send requests and observe responses in terms of HTTP types. let (clientTestChannel, clientNegotiatedConnectionFuture) = try await self.setUpClientConnection( - tlsConfiguration: clientTLSConfiguration + tlsConfig: tlsConfig ) try await withThrowingDiscardingTaskGroup { group in @@ -117,7 +129,7 @@ struct HTTPSecureUpgradeClientServerProvider { } private func setUpClientConnection( - tlsConfiguration: TLSConfiguration + tlsConfig: TLSConfiguration ) async throws -> ( NIOAsyncTestingChannel, EventLoopFuture< @@ -128,30 +140,14 @@ struct HTTPSecureUpgradeClientServerProvider { ) { let clientTestChannel = try await NIOAsyncTestingChannel { channel in _ = channel.eventLoop.makeCompletedFuture { - try channel.pipeline.syncOperations.addHandler( - try NIOSSLClientHandler(context: .init(configuration: tlsConfiguration), serverHostname: nil) - ) + try NIOSecureUpgradeClient.sslClientChannelInitializer(channel, tlsConfig: tlsConfig) } } - let clientNegotiatedConnection = try await clientTestChannel.eventLoop.flatSubmit { - clientTestChannel.configureHTTP2AsyncSecureUpgrade( - http1ConnectionInitializer: { channel in - channel.eventLoop.makeCompletedFuture { - try channel.pipeline.syncOperations.addHTTPClientHandlers() - try channel.pipeline.syncOperations.addHandlers(HTTP1ToHTTPClientCodec()) - - return try NIOAsyncChannel( - wrappingChannelSynchronously: channel, - configuration: .init(isOutboundHalfClosureEnabled: true) - ) - } - }, - http2ConnectionInitializer: { channel in - channel.configureAsyncHTTP2Pipeline(mode: .client) { $0.eventLoop.makeSucceededFuture($0) } - } - ) - }.get() + let clientNegotiatedConnection = try await NIOSecureUpgradeClient.clientChannelInitializer( + clientTestChannel, + tlsConfig: tlsConfig + ).get() let connectionPromise = clientTestChannel.eventLoop.makePromise(of: Void.self) clientTestChannel.connect(to: try .init(ipAddress: "127.0.0.1", port: 8000), promise: connectionPromise) @@ -160,41 +156,3 @@ struct HTTPSecureUpgradeClientServerProvider { return (clientTestChannel, clientNegotiatedConnection) } } - -enum NegotiatedConnection { - case http1(NIOAsyncChannel) - case http2(HTTP2StreamManager) - - init( - negotiationResult: NIONegotiatedHTTPVersion< - NIOAsyncChannel, NIOHTTP2Handler.AsyncStreamMultiplexer - > - ) async throws { - switch negotiationResult { - case .http1_1(let http1AsyncChannel): - self = .http1(http1AsyncChannel) - - case .http2(let http2StreamMultiplexer): - self = .http2(.init(http2StreamMultiplexer: http2StreamMultiplexer)) - } - } - - struct HTTP2StreamManager { - let http2StreamMultiplexer: NIOHTTP2Handler.AsyncStreamMultiplexer - - /// A wrapper over `NIOHTTP2Handler/AsyncStreamMultiplexer/openStream(_:)` that first initializes the stream - /// channel with the `HTTP2FramePayloadToHTTPClientCodec` channel handler, and wraps it in a `NIOAsyncChannel` - /// (with outbound half closure enabled). - func openStream() async throws -> NIOAsyncChannel { - try await self.http2StreamMultiplexer.openStream { channel in - channel.eventLoop.makeCompletedFuture { - try channel.pipeline.syncOperations.addHandler(HTTP2FramePayloadToHTTPClientCodec()) - return try NIOAsyncChannel( - wrappingChannelSynchronously: channel, - configuration: .init(isOutboundHalfClosureEnabled: true) - ) - } - } - } - } -} From 2f6a090aa9ba76f1f29255c7cf5efd2cb4d31bfd Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Wed, 11 Feb 2026 14:56:19 +0000 Subject: [PATCH 02/13] More refactoring --- .../NIOHTTPServerEndToEndTests.swift | 19 +- .../NIOHTTPServerTests.swift | 236 +++++++++--------- .../Utilities/NIOClient/NIOClient+HTTP1.swift | 2 +- .../NIOClient/NIOClient+SecureUpgrade.swift | 10 +- .../TestingClientServerProvider+HTTP1.swift | 138 +++++----- ...ngClientServerProvider+SecureUpgrade.swift | 152 ++++++----- 6 files changed, 281 insertions(+), 276 deletions(-) diff --git a/Tests/NIOHTTPServerTests/NIOHTTPServerEndToEndTests.swift b/Tests/NIOHTTPServerTests/NIOHTTPServerEndToEndTests.swift index fabf397..da36778 100644 --- a/Tests/NIOHTTPServerTests/NIOHTTPServerEndToEndTests.swift +++ b/Tests/NIOHTTPServerTests/NIOHTTPServerEndToEndTests.swift @@ -32,7 +32,7 @@ struct NIOHTTPServerEndToEndTests { @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) @Test("HTTP/1.1 request and response") func testHTTP1_1() async throws { - try await HTTP1TestingChannelClientServerProvider.withProvider( + try await TestingChannelServer.withPlaintextHTTP1Client( logger: Logger(label: "NIOHTTPServerEndToEndTests"), serverRequestHandler: HTTPServerClosureRequestHandler { request, reqContext, reqReader, resSender in let sender = try await resSender.send(.init(status: .ok)) @@ -43,9 +43,9 @@ struct NIOHTTPServerEndToEndTests { return [.serverTiming: "test"] } } - ) { clientServerProvider in - try await clientServerProvider.withConnection { client in - try await client.executeThenClose { inbound, outbound in + ) { client in + try await client.withConnection { connectionChannel in + try await connectionChannel.executeThenClose { inbound, outbound in try await outbound.write(.head(.init(method: .get, scheme: "", authority: "", path: "/"))) try await outbound.write(.end(nil)) @@ -79,7 +79,7 @@ struct NIOHTTPServerEndToEndTests { @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) @Test("HTTP/2 negotiation") - func testSecureUpgradeNegotiation() async throws { + func testHTTP2Negotiation() async throws { let serverChain = try TestCA.makeSelfSignedChain() var serverTLSConfig = TLSConfiguration.makeServerConfiguration( certificateChain: [try .init(serverChain.leaf)], @@ -92,7 +92,7 @@ struct NIOHTTPServerEndToEndTests { clientTLSConfig.certificateVerification = .noHostnameVerification clientTLSConfig.applicationProtocols = ["h2"] - try await HTTPSecureUpgradeClientServerProvider.withProvider( + try await TestingChannelServer.withSecureUpgradeClient( logger: Logger(label: "NIOHTTPServerEndToEndTests"), tlsConfiguration: serverTLSConfig, handler: HTTPServerClosureRequestHandler { request, reqContext, reqReader, resSender in @@ -104,10 +104,9 @@ struct NIOHTTPServerEndToEndTests { return [.serverTiming: "test"] } } - ) { clientServerProvider in - try await clientServerProvider.withConnectedClient(tlsConfig: clientTLSConfig) { - negotiatedConnection in - switch negotiatedConnection { + ) { client in + try await client.withConnection(tlsConfig: clientTLSConfig) { negotiatedConnectionChannel in + switch negotiatedConnectionChannel { case .http1(_): Issue.record("Failed to negotiate HTTP/2 despite the client requiring HTTP/2.") diff --git a/Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift b/Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift index 3327c59..1f847a3 100644 --- a/Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift +++ b/Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import HTTPServer import HTTPTypes import Logging import NIOCore @@ -57,19 +58,15 @@ struct NIOHTTPServerTests { configuration: .init(bindTarget: .hostAndPort(host: "127.0.0.1", port: 1234)) ) - try await withThrowingTaskGroup { group in - group.addTask { - try await server.serve { _, _, _, _ in } + try await Self.withServer( + server: server, + serverHandler: HTTPServerClosureRequestHandler { _, _, _, _ in }, + body: { serverAddress in + let address = try #require(serverAddress.ipv4) + #expect(address.host == "127.0.0.1") + #expect(address.port == 1234) } - - let serverAddress = try await server.listeningAddress - - let address = try #require(serverAddress.ipv4) - #expect(address.host == "127.0.0.1") - #expect(address.port == 1234) - - group.cancelAll() - } + ) // Now that the server has shut down, try obtaining the listening address. This should result in an error. await #expect(throws: ListeningAddressError.serverClosed) { @@ -85,51 +82,47 @@ struct NIOHTTPServerTests { configuration: .init(bindTarget: .hostAndPort(host: "127.0.0.1", port: 0)) ) - try await withThrowingTaskGroup { group in - group.addTask { - try await server.serve { request, requestContext, reader, responseWriter in - #expect(request.method == .post) - #expect(request.path == "/") - - var buffer = ByteBuffer() - let (_, finalElement) = try await reader.consumeAndConclude { bodyReader in - var bodyReader = bodyReader - return try await bodyReader.collect(upTo: Self.bodyData.readableBytes + 1) { body in - buffer.writeBytes(body.bytes) - } - } - #expect(buffer == Self.bodyData) - #expect(finalElement == Self.trailer) - - let responseBodySender = try await responseWriter.send(.init(status: .ok)) - try await responseBodySender.produceAndConclude { responseBodyWriter in - var responseBodyWriter = responseBodyWriter - try await responseBodyWriter.write([1, 2].span) - return Self.trailer + try await Self.withServer( + server: server, + serverHandler: HTTPServerClosureRequestHandler { request, requestContext, reader, responseWriter in + #expect(request.method == .post) + #expect(request.path == "/") + + var buffer = ByteBuffer() + let (_, finalElement) = try await reader.consumeAndConclude { bodyReader in + var bodyReader = bodyReader + return try await bodyReader.collect(upTo: Self.bodyData.readableBytes + 1) { body in + buffer.writeBytes(body.bytes) } } - } + #expect(buffer == Self.bodyData) + #expect(finalElement == Self.trailer) + + let responseBodySender = try await responseWriter.send(.init(status: .ok)) + try await responseBodySender.produceAndConclude { responseBodyWriter in + var responseBodyWriter = responseBodyWriter + try await responseBodyWriter.write([1, 2].span) + return Self.trailer + } + }, + body: { serverAddress in + let client = try await NIOHTTP1Client.setUpChannel(at: serverAddress) + try await client.executeThenClose { inbound, outbound in + try await outbound.write(Self.reqHead) + try await outbound.write(Self.reqBody) + try await outbound.write(Self.reqEnd) - let serverAddress = try await server.listeningAddress - - let client = try await NIOHTTP1Client.setUpClient(at: serverAddress) - try await client.executeThenClose { inbound, outbound in - try await outbound.write(Self.reqHead) - try await outbound.write(Self.reqBody) - try await outbound.write(Self.reqEnd) - - for try await response in inbound { - try await Self.clientResponseHandler( - response, - expectedStatus: .ok, - expectedBody: .init([1, 2]), - expectedTrailers: Self.trailer - ) + for try await response in inbound { + try await Self.clientResponseHandler( + response, + expectedStatus: .ok, + expectedBody: .init([1, 2]), + expectedTrailers: Self.trailer + ) + } } } - - group.cancelAll() - } + ) } @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) @@ -158,80 +151,63 @@ struct NIOHTTPServerTests { ) ) - try await withThrowingTaskGroup { group in - group.addTask { - try await server.serve { request, requestContext, reader, responseWriter in - #expect(request.method == .post) - #expect(request.path == "/") - - do { - let peerChain = try #require(try await NIOHTTPServer.connectionContext.peerCertificateChain) - #expect(Array(peerChain) == [clientChain.leaf]) - } catch { - Issue.record("Could not obtain the peer's certificate chain: \(error)") - } + try await Self.withServer( + server: server, + serverHandler: HTTPServerClosureRequestHandler { request, requestContext, reader, responseWriter in + #expect(request.method == .post) + #expect(request.path == "/") + + do { + let peerChain = try #require(try await NIOHTTPServer.connectionContext.peerCertificateChain) + #expect(Array(peerChain) == [clientChain.leaf]) + } catch { + Issue.record("Could not obtain the peer's certificate chain: \(error)") + } - let (buffer, finalElement) = try await reader.consumeAndConclude { bodyReader in - var bodyReader = bodyReader - var buffer = ByteBuffer() - _ = try await bodyReader.collect(upTo: Self.bodyData.readableBytes + 1) { body in - buffer.writeBytes(body.bytes) - } - return buffer - } - #expect(buffer == Self.bodyData) - #expect(finalElement == Self.trailer) - - let sender = try await responseWriter.send(.init(status: .ok)) - try await sender.produceAndConclude { bodyWriter in - var bodyWriter = bodyWriter - try await bodyWriter.write([1, 2].span) - return Self.trailer + let (buffer, finalElement) = try await reader.consumeAndConclude { bodyReader in + var bodyReader = bodyReader + var buffer = ByteBuffer() + _ = try await bodyReader.collect(upTo: Self.bodyData.readableBytes + 1) { body in + buffer.writeBytes(body.bytes) } + return buffer } - } - - let serverAddress = try await server.listeningAddress - - let clientChannel = try await NIOSecureUpgradeClient.setUpMTLSClient( - at: serverAddress, - clientChain: clientChain, - trustRoots: [serverChain.ca], - applicationProtocol: applicationProtocol - ) - - switch clientChannel { - case .http1(let client): - guard applicationProtocol == "http/1.1" else { - Issue.record("Unexpectedly negotiated a HTTP/1.1 connection") - return + #expect(buffer == Self.bodyData) + #expect(finalElement == Self.trailer) + + let sender = try await responseWriter.send(.init(status: .ok)) + try await sender.produceAndConclude { bodyWriter in + var bodyWriter = bodyWriter + try await bodyWriter.write([1, 2].span) + return Self.trailer } + }, + body: { serverAddress in + let client = try await NIOSecureUpgradeClient.setUpMTLSChannel( + at: serverAddress, + clientChain: clientChain, + trustRoots: [serverChain.ca], + applicationProtocol: applicationProtocol + ) - try await client.executeThenClose { inbound, outbound in - try await outbound.write(Self.reqHead) - try await outbound.write(Self.reqBody) - try await outbound.write(Self.reqEnd) - - for try await response in inbound { - try await Self.clientResponseHandler( - response, - expectedStatus: .ok, - expectedBody: .init([1, 2]), - expectedTrailers: Self.trailer - ) + let clientChannel: NIOAsyncChannel + switch client { + case .http1(let http1ClientChannel): + guard applicationProtocol == "http/1.1" else { + Issue.record("Unexpectedly negotiated a HTTP/1.1 connection") + return } - } - // Cancel the server and client task once we know the client has received the response - group.cancelAll() + clientChannel = http1ClientChannel - case .http2(let streamManager): - guard applicationProtocol == "h2" else { - Issue.record("Unexpectedly negotiated a HTTP/2 connection") - return + case .http2(let streamManager): + guard applicationProtocol == "h2" else { + Issue.record("Unexpectedly negotiated a HTTP/2 connection") + return + } + clientChannel = try await streamManager.openStream() } - let streamChannel = try await streamManager.openStream() - try await streamChannel.executeThenClose { inbound, outbound in + try await clientChannel.executeThenClose { inbound, outbound in try await outbound.write(Self.reqHead) try await outbound.write(Self.reqBody) try await outbound.write(Self.reqEnd) @@ -245,9 +221,31 @@ struct NIOHTTPServerTests { ) } } - // Cancel the server and client task once we know the client has received the response - group.cancelAll() } + ) + } +} + +@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) +extension NIOHTTPServerTests { + static func withServer( + server: NIOHTTPServer, + serverHandler: some HTTPServerRequestHandler, + body: (NIOHTTPServer.SocketAddress) async throws -> Void + ) async throws { + try await withThrowingTaskGroup { group in + // Add the server task to the group + group.addTask { + try await server.serve(handler: serverHandler) + } + + // Wait for the server to start listening before running the body closure + let listeningAddress = try await server.listeningAddress + + try await body(listeningAddress) + + // Shut the server down + group.cancelAll() } } } diff --git a/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+HTTP1.swift b/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+HTTP1.swift index 72843e3..210a6ab 100644 --- a/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+HTTP1.swift +++ b/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+HTTP1.swift @@ -32,7 +32,7 @@ struct NIOHTTP1Client { } /// Creates and connects an HTTP/1.1 client to the specified address. - static func setUpClient( + static func setUpChannel( at address: NIOHTTPServer.SocketAddress ) async throws -> NIOAsyncChannel { try await ClientBootstrap(group: .singletonMultiThreadedEventLoopGroup) diff --git a/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+SecureUpgrade.swift b/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+SecureUpgrade.swift index e50ae60..f9db21b 100644 --- a/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+SecureUpgrade.swift +++ b/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+SecureUpgrade.swift @@ -36,18 +36,18 @@ struct NIOSecureUpgradeClient { } /// Creates and connects a TLS-enabled client to the specified address with ALPN negotiation. - static func setUpClient( + static func setUpChannel( at address: NIOHTTPServer.SocketAddress, trustRoots: [Certificate], applicationProtocol: String ) async throws -> NegotiatedClientConnection { let tlsConfig = try self.setUpTLSConfig(trustRoots: trustRoots, applicationProtocol: applicationProtocol) - return try await self._setUpClient(at: address, tlsConfig: tlsConfig) + return try await self._setUpChannel(at: address, tlsConfig: tlsConfig) } /// Exactly like ``setUpClient(at:trustRoots:applicationProtocol:)`` but with mTLS enabled. - static func setUpMTLSClient( + static func setUpMTLSChannel( at address: NIOHTTPServer.SocketAddress, clientChain: ChainPrivateKeyPair, trustRoots: [Certificate], @@ -57,11 +57,11 @@ struct NIOSecureUpgradeClient { tlsConfig.certificateChain = [try NIOSSLCertificateSource(clientChain.leaf)] tlsConfig.privateKey = .privateKey(try .init(clientChain.privateKey)) - return try await self._setUpClient(at: address, tlsConfig: tlsConfig) + return try await self._setUpChannel(at: address, tlsConfig: tlsConfig) } /// Creates and connects a client to the specified address with the provided TLS configuration. - private static func _setUpClient( + private static func _setUpChannel( at address: NIOHTTPServer.SocketAddress, tlsConfig: TLSConfiguration ) async throws -> NegotiatedClientConnection { diff --git a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+HTTP1.swift b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+HTTP1.swift index 9f2e3da..466f96d 100644 --- a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+HTTP1.swift +++ b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+HTTP1.swift @@ -23,36 +23,32 @@ import X509 @testable import NIOHTTPServer -/// A testing utility that sets up a `NIOHTTPServer` instance (with the HTTP/1.1 transport) based on -/// `NIOAsyncTestingChannel` (instead of the `ServerSocketChannel` that `NIOHTTPServer` normally uses) and vends a -/// client instance for sending requests and observing responses. +/// A testing utility that sets up a `NIOHTTPServer` instance based on `NIOAsyncTestingChannel` (instead of the +/// `ServerSocketChannel` that `NIOHTTPServer` normally uses) and vends a client instance for sending requests and +/// observing responses. /// /// This provider creates a `NIOHTTPServer` instance using a `NIOAsyncTestingChannel` as its listening channel. Since no /// network socket is actually listening for incoming connections, client connections are simulated by *writing* a /// connection channel to the server channel. This connection channel is set up with the same handlers that /// `ServerBootstrap` would set up and vend to `NIOHTTPServer` on an incoming connection. /// -/// This provider vends a NIO HTTP client channel (also backed by a `NIOAsyncTestingChannel`) that can be used to send -/// requests and observer response in terms of HTTP types (`HTTPRequestPart` and `HTTPResponsePart`) to the server +/// This provider vends a HTTP client channel (also backed by a `NIOAsyncTestingChannel`) that can be used to send +/// requests and observe responses in terms of HTTP types (`HTTPRequestPart` and `HTTPResponsePart`) to the server /// connection channel. @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) -struct HTTP1TestingChannelClientServerProvider { - let server: NIOHTTPServer - let serverTestChannel: NIOAsyncTestingChannel - - /// Sets up the server with a testing channel and the provided request handler, starts the server, and provides - /// `Self` to the `body` closure. Call `withConnection(_:)` on the provided instance to simulate incoming - /// connections. - static func withProvider( +struct TestingChannelServer { + /// Creates a `NIOHTTPServer` backed by a `NIOAsyncTestingChannel` and the provided request handler, starts it, and + /// provides a `HTTP1Client` to the `body` closure. + static func withPlaintextHTTP1Client( logger: Logger, serverRequestHandler: some HTTPServerRequestHandler< HTTPRequestConcludingAsyncReader, HTTPResponseConcludingAsyncWriter >, - body: (Self) async throws -> Void + body: (PlaintextHTTP1Client) async throws -> Void ) async throws { let server = NIOHTTPServer( logger: logger, - // The server won't actually be bound to this host and port; we just have to pass this argument + // The server won't actually be bound to this host and port; we just have to pass this argument. configuration: .init(bindTarget: .hostAndPort(host: "127.0.0.1", port: 8000)) ) // Create a test channel. We will run the server on this channel. @@ -67,71 +63,89 @@ struct HTTP1TestingChannelClientServerProvider { ) } - // Execute the provided closure with a `HTTP1ClientServerProvider` instance created from the server - // instance and the test channel instance - try await body(Self(server: server, serverTestChannel: serverTestChannel)) + // Execute the provided closure with a `PlaintextHTTP1Client` instance created from the server instance and + // the test channel instance. + try await body(PlaintextHTTP1Client(server: server, serverTestChannel: serverTestChannel)) group.cancelAll() } } - /// Starts a new connection with the server and executes the provided `body` closure. - /// - Parameter body: A closure that should send a request using the provided client instance and validate the - /// received response. - func withConnection( - body: (NIOAsyncChannel) async throws -> Void - ) async throws { - // Create a connection channel that we will write to the server. + /// Creates a connection channel that can be written to the server channel to simulate an incoming connection. + static func createServerConnectionChannel() async throws -> NIOAsyncTestingChannel { let serverTestConnectionChannel = NIOAsyncTestingChannel() let connectionPromise = serverTestConnectionChannel.eventLoop.makePromise(of: Void.self) + // The `to` address has no significance here, it is just a random address. We are just interested in making the + // channel "active"; calling `connect` is the way to achieve that. serverTestConnectionChannel.connect( to: try .init(ipAddress: "127.0.0.1", port: 8000), promise: connectionPromise ) try await connectionPromise.futureResult.get() - // Set up the required channel handlers on `serverTestConnectionChannel` - let serverAsyncConnectionChannel = try await self.server.setupHTTP1_1ConnectionChildChannel( - channel: serverTestConnectionChannel, - asyncChannelConfiguration: .init() - ).get() - - // Write the connection channel to the server channel to simulate an incoming connection - try await self.serverTestChannel.writeInbound(serverAsyncConnectionChannel) - - // Now, we could write requests directly to `serverAsyncConnectionChannel`, but it expects `ByteBuffer` inputs. - // This is cumbersome to work with in tests. - // So, we create a client channel, and use it to send requests and observe responses in terms of HTTP types. - let (clientTestChannel, clientAsyncChannel) = try await self.setUpClientConnection() - - try await withThrowingDiscardingTaskGroup { group in - // We must forward all client outbound writes to the server and vice-versa. - group.addTask { try await clientTestChannel.glueTo(serverTestConnectionChannel) } - - try await body(clientAsyncChannel) - - try await serverTestConnectionChannel.close() - } + return serverTestConnectionChannel } +} - /// Creates a client testing channel configured with HTTP channel handlers and wraps it in a `NIOAsyncChannel`. - private func setUpClientConnection() async throws -> ( - NIOAsyncTestingChannel, - NIOAsyncChannel - ) { - let clientTestChannel = try await NIOAsyncTestingChannel { channel in - try NIOHTTP1Client.clientChannelInitializer(channel) +@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) +extension TestingChannelServer { + /// A plaintext HTTP/1.1 client backed by a `NIOAsyncTestingChannel`. + struct PlaintextHTTP1Client { + let server: NIOHTTPServer + let serverTestChannel: NIOAsyncTestingChannel + + /// Starts a new connection with the server and executes the provided `body` closure. + /// - Parameter body: A closure that should send a request using the provided client channel and validate the + /// received response. + func withConnection( + body: (NIOAsyncChannel) async throws -> Void + ) async throws { + // Create a connection channel: we will write this to the server channel to simulate an incoming connection + let serverTestConnectionChannel = try await TestingChannelServer.createServerConnectionChannel() + + // Set up the required channel handlers on `serverTestConnectionChannel` + let serverAsyncConnectionChannel = try await self.server.setupHTTP1_1ConnectionChildChannel( + channel: serverTestConnectionChannel, + asyncChannelConfiguration: .init() + ).get() + + // Write the connection channel to the server channel to simulate an incoming connection + try await self.serverTestChannel.writeInbound(serverAsyncConnectionChannel) + + // Now, we could write requests directly to `serverAsyncConnectionChannel`, but it expects `ByteBuffer` + // inputs. This is cumbersome to work with in tests, so we create a client channel, and use it to send + // requests and observe responses in terms of HTTP types. + let (clientTestChannel, clientAsyncChannel) = try await self.setUpClientConnection() + + try await withThrowingDiscardingTaskGroup { group in + // We must forward all client outbound writes to the server and vice-versa. + group.addTask { try await clientTestChannel.glueTo(serverTestConnectionChannel) } + + try await body(clientAsyncChannel) + + try await serverTestConnectionChannel.close() + } } - // Wrap the client channel in a NIOAsyncChannel for convenience - let clientAsyncChannel = try await clientTestChannel.eventLoop.submit { - try NIOAsyncChannel( - wrappingChannelSynchronously: clientTestChannel, - configuration: .init(isOutboundHalfClosureEnabled: true) - ) - }.get() + /// Creates a client testing channel configured with HTTP channel handlers and wraps it in a `NIOAsyncChannel`. + private func setUpClientConnection() async throws -> ( + NIOAsyncTestingChannel, + NIOAsyncChannel + ) { + let clientTestChannel = try await NIOAsyncTestingChannel { channel in + try NIOHTTP1Client.clientChannelInitializer(channel) + } - return (clientTestChannel, clientAsyncChannel) + // Wrap the client channel in a NIOAsyncChannel for convenience + let clientAsyncChannel = try await clientTestChannel.eventLoop.submit { + try NIOAsyncChannel( + wrappingChannelSynchronously: clientTestChannel, + configuration: .init(isOutboundHalfClosureEnabled: true) + ) + }.get() + + return (clientTestChannel, clientAsyncChannel) + } } } diff --git a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+SecureUpgrade.swift b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+SecureUpgrade.swift index 332767e..c3b6059 100644 --- a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+SecureUpgrade.swift +++ b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+SecureUpgrade.swift @@ -25,31 +25,18 @@ import X509 @testable import NIOHTTPServer -/// A testing utility that sets up a `NIOHTTPServer` instance (with the secure upgrade transport) based on -/// `NIOAsyncTestingChannel` (instead of the `ServerSocketChannel` that `NIOHTTPServer` normally uses) and vends a -/// client instance for sending requests and observing responses. -/// -/// Like ``HTTP1ClientServerProvider``, but for the secure upgrade transport. @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) -struct HTTPSecureUpgradeClientServerProvider { - let server: NIOHTTPServer - let serverTestChannel: NIOAsyncTestingChannel - - let serverTLSConfiguration: TLSConfiguration - let verificationCallback: (@Sendable ([Certificate]) async throws -> CertificateVerificationResult)? - - let http2Configuration: NIOHTTP2Handler.Configuration - +extension TestingChannelServer { /// Sets up the server with a testing channel and the provided request handler, starts the server, and provides /// `Self` to the `body` closure. Call `withConnectedClient(clientTLSConfiguration:body:)` on the provided instance /// to simulate incoming connections. - static func withProvider( + static func withSecureUpgradeClient( logger: Logger, tlsConfiguration: TLSConfiguration, tlsVerificationCallback: (@Sendable ([Certificate]) async throws -> CertificateVerificationResult)? = nil, http2Configuration: NIOHTTP2Handler.Configuration = .init(), handler: some HTTPServerRequestHandler, - body: (HTTPSecureUpgradeClientServerProvider) async throws -> Void + body: (SecureUpgradeClient) async throws -> Void ) async throws { let server = NIOHTTPServer( logger: logger, @@ -66,9 +53,9 @@ struct HTTPSecureUpgradeClientServerProvider { try await server.serveSecureUpgradeWithTestChannel(testChannel: serverTestChannel, handler: handler) } - // Execute the provided closure with a `HTTPSecureUpgradeClientServerProvider` instance + // Execute the provided closure with a `SecureUpgradeClient` instance try await body( - HTTPSecureUpgradeClientServerProvider( + SecureUpgradeClient( server: server, serverTestChannel: serverTestChannel, serverTLSConfiguration: tlsConfiguration, @@ -80,79 +67,86 @@ struct HTTPSecureUpgradeClientServerProvider { group.cancelAll() } } +} - /// Starts a new TLS connection with ALPN negotiation to the server and executes the provided `body` closure with - /// the negotiated ALPN result as an argument. - func withConnectedClient( - tlsConfig: TLSConfiguration, - body: (NegotiatedClientConnection) async throws -> Void - ) async throws { - // Create a test connection channel - let serverTestConnectionChannel = NIOAsyncTestingChannel() - - let connectionPromise = serverTestConnectionChannel.eventLoop.makePromise(of: Void.self) - serverTestConnectionChannel.connect( - to: try .init(ipAddress: "127.0.0.1", port: 8000), - promise: connectionPromise - ) - try await connectionPromise.futureResult.get() - - // Set up the required channel handlers on `serverTestConnectionChannel` - let negotiatedServerConnectionFuture = try await serverTestConnectionChannel.eventLoop.flatSubmit { - self.server.setupSecureUpgradeConnectionChildChannel( - channel: serverTestConnectionChannel, - tlsConfiguration: self.serverTLSConfiguration, - asyncChannelConfiguration: .init(), - http2Configuration: self.http2Configuration, - verificationCallback: self.verificationCallback - ) - }.get() +@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) +extension TestingChannelServer { + // A Secure Upgrade HTTP client backed by a `NIOAsyncTestingChannel`. + struct SecureUpgradeClient { + let server: NIOHTTPServer + let serverTestChannel: NIOAsyncTestingChannel + + let serverTLSConfiguration: TLSConfiguration + let verificationCallback: (@Sendable ([Certificate]) async throws -> CertificateVerificationResult)? + + let http2Configuration: NIOHTTP2Handler.Configuration + + /// Starts a new TLS connection with ALPN negotiation to the server and executes the provided `body` closure + /// with the negotiated ALPN result as an argument. + func withConnection( + tlsConfig: TLSConfiguration, + body: (NegotiatedClientConnection) async throws -> Void + ) async throws { + // Create a connection channel: we will write this to the server channel to simulate an incoming connection. + let serverTestConnectionChannel = try await TestingChannelServer.createServerConnectionChannel() + + // Set up the required channel handlers on `serverTestConnectionChannel` + let negotiatedServerConnectionFuture = try await serverTestConnectionChannel.eventLoop.flatSubmit { + self.server.setupSecureUpgradeConnectionChildChannel( + channel: serverTestConnectionChannel, + tlsConfiguration: self.serverTLSConfiguration, + asyncChannelConfiguration: .init(), + http2Configuration: self.http2Configuration, + verificationCallback: self.verificationCallback + ) + }.get() - // Write the connection channel to the server channel to simulate an incoming connection - try await self.serverTestChannel.writeInbound(negotiatedServerConnectionFuture) + // Write the connection channel to the server channel to simulate an incoming connection + try await self.serverTestChannel.writeInbound(negotiatedServerConnectionFuture) - // Now we could write requests directly to the server channel, but it expects `ByteBuffer` inputs. This is - // cumbersome to work with in tests. - // So, we create a client channel, and use it to send requests and observe responses in terms of HTTP types. - let (clientTestChannel, clientNegotiatedConnectionFuture) = try await self.setUpClientConnection( - tlsConfig: tlsConfig - ) + // Now, we could write requests directly to `serverAsyncConnectionChannel`, but it expects `ByteBuffer` + // inputs. This is cumbersome to work with in tests, so we create a client channel, and use it to send + // requests and observe responses in terms of HTTP types. + let (clientTestChannel, clientNegotiatedConnectionFuture) = try await self.setUpClientConnection( + tlsConfig: tlsConfig + ) - try await withThrowingDiscardingTaskGroup { group in - // We must forward all client outbound writes to the server and vice-versa. - group.addTask { try await clientTestChannel.glueTo(serverTestConnectionChannel) } + try await withThrowingDiscardingTaskGroup { group in + // We must forward all client outbound writes to the server and vice-versa. + group.addTask { try await clientTestChannel.glueTo(serverTestConnectionChannel) } - try await body(.init(negotiationResult: try await clientNegotiatedConnectionFuture.get())) + try await body(.init(negotiationResult: try await clientNegotiatedConnectionFuture.get())) - try await serverTestConnectionChannel.close() + try await serverTestConnectionChannel.close() + } } - } - private func setUpClientConnection( - tlsConfig: TLSConfiguration - ) async throws -> ( - NIOAsyncTestingChannel, - EventLoopFuture< - NIONegotiatedHTTPVersion< - NIOAsyncChannel, NIOHTTP2Handler.AsyncStreamMultiplexer + private func setUpClientConnection( + tlsConfig: TLSConfiguration + ) async throws -> ( + NIOAsyncTestingChannel, + EventLoopFuture< + NIONegotiatedHTTPVersion< + NIOAsyncChannel, NIOHTTP2Handler.AsyncStreamMultiplexer + > > - > - ) { - let clientTestChannel = try await NIOAsyncTestingChannel { channel in - _ = channel.eventLoop.makeCompletedFuture { - try NIOSecureUpgradeClient.sslClientChannelInitializer(channel, tlsConfig: tlsConfig) + ) { + let clientTestChannel = try await NIOAsyncTestingChannel { channel in + _ = channel.eventLoop.makeCompletedFuture { + try NIOSecureUpgradeClient.sslClientChannelInitializer(channel, tlsConfig: tlsConfig) + } } - } - let clientNegotiatedConnection = try await NIOSecureUpgradeClient.clientChannelInitializer( - clientTestChannel, - tlsConfig: tlsConfig - ).get() + let clientNegotiatedConnection = try await NIOSecureUpgradeClient.clientChannelInitializer( + clientTestChannel, + tlsConfig: tlsConfig + ).get() - let connectionPromise = clientTestChannel.eventLoop.makePromise(of: Void.self) - clientTestChannel.connect(to: try .init(ipAddress: "127.0.0.1", port: 8000), promise: connectionPromise) - try await connectionPromise.futureResult.get() + let connectionPromise = clientTestChannel.eventLoop.makePromise(of: Void.self) + clientTestChannel.connect(to: try .init(ipAddress: "127.0.0.1", port: 8000), promise: connectionPromise) + try await connectionPromise.futureResult.get() - return (clientTestChannel, clientNegotiatedConnection) + return (clientTestChannel, clientNegotiatedConnection) + } } } From c75eba14c1cce7bf9f9f8439ee35b67619c1fbec Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Thu, 12 Feb 2026 09:49:23 +0000 Subject: [PATCH 03/13] More refactoring --- .../NIOHTTPServerEndToEndTests.swift | 4 +- .../Utilities/Helpers.swift | 16 ++ .../TestingChannelClient+HTTP1.swift | 81 ++++++++++ ... TestingChannelClient+SecureUpgrade.swift} | 54 +------ .../TestingChannelServer+HTTP1.swift | 66 ++++++++ .../TestingChannelServer+SecureUpgrade.swift | 67 ++++++++ .../TestingClientServerProvider+HTTP1.swift | 151 ------------------ 7 files changed, 235 insertions(+), 204 deletions(-) create mode 100644 Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelClient+HTTP1.swift rename Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/{TestingClientServerProvider+SecureUpgrade.swift => TestingChannelClient+SecureUpgrade.swift} (65%) create mode 100644 Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+HTTP1.swift create mode 100644 Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift delete mode 100644 Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+HTTP1.swift diff --git a/Tests/NIOHTTPServerTests/NIOHTTPServerEndToEndTests.swift b/Tests/NIOHTTPServerTests/NIOHTTPServerEndToEndTests.swift index da36778..950eb45 100644 --- a/Tests/NIOHTTPServerTests/NIOHTTPServerEndToEndTests.swift +++ b/Tests/NIOHTTPServerTests/NIOHTTPServerEndToEndTests.swift @@ -32,7 +32,7 @@ struct NIOHTTPServerEndToEndTests { @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) @Test("HTTP/1.1 request and response") func testHTTP1_1() async throws { - try await TestingChannelServer.withPlaintextHTTP1Client( + try await TestingChannelHTTP1Server.withClient( logger: Logger(label: "NIOHTTPServerEndToEndTests"), serverRequestHandler: HTTPServerClosureRequestHandler { request, reqContext, reqReader, resSender in let sender = try await resSender.send(.init(status: .ok)) @@ -92,7 +92,7 @@ struct NIOHTTPServerEndToEndTests { clientTLSConfig.certificateVerification = .noHostnameVerification clientTLSConfig.applicationProtocols = ["h2"] - try await TestingChannelServer.withSecureUpgradeClient( + try await TestingChannelSecureUpgradeServer.withClient( logger: Logger(label: "NIOHTTPServerEndToEndTests"), tlsConfiguration: serverTLSConfig, handler: HTTPServerClosureRequestHandler { request, reqContext, reqReader, resSender in diff --git a/Tests/NIOHTTPServerTests/Utilities/Helpers.swift b/Tests/NIOHTTPServerTests/Utilities/Helpers.swift index d973368..0bd80db 100644 --- a/Tests/NIOHTTPServerTests/Utilities/Helpers.swift +++ b/Tests/NIOHTTPServerTests/Utilities/Helpers.swift @@ -36,4 +36,20 @@ extension NIOAsyncTestingChannel { } } } + + /// Creates a connection channel that can be written to the server channel to simulate an incoming connection. + static func createActiveChannel() async throws -> NIOAsyncTestingChannel { + let serverTestConnectionChannel = NIOAsyncTestingChannel() + + let connectionPromise = serverTestConnectionChannel.eventLoop.makePromise(of: Void.self) + // The `to` address has no significance here, it is just a random address. We are only interested in making the + // channel *active*; calling `connect` is the way to achieve that. + serverTestConnectionChannel.connect( + to: try .init(ipAddress: "127.0.0.1", port: 8000), + promise: connectionPromise + ) + try await connectionPromise.futureResult.get() + + return serverTestConnectionChannel + } } diff --git a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelClient+HTTP1.swift b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelClient+HTTP1.swift new file mode 100644 index 0000000..57e6a6d --- /dev/null +++ b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelClient+HTTP1.swift @@ -0,0 +1,81 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP Server open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore +import NIOEmbedded +import NIOHTTPTypes + +@testable import NIOHTTPServer + +@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) +extension TestingChannelHTTP1Server { + /// A plaintext HTTP/1.1 client backed by a `NIOAsyncTestingChannel`. + struct Client { + let server: NIOHTTPServer + let serverTestChannel: NIOAsyncTestingChannel + + /// Starts a new connection with the server and executes the provided `body` closure. + /// - Parameter body: A closure that should send a request using the provided client channel and validate the + /// received response. + func withConnection( + body: (NIOAsyncChannel) async throws -> Void + ) async throws { + // Create a connection channel: we will write this to the server channel to simulate an incoming connection + let serverTestConnectionChannel = try await NIOAsyncTestingChannel.createActiveChannel() + + // Set up the required channel handlers on `serverTestConnectionChannel` + let serverAsyncConnectionChannel = try await self.server.setupHTTP1_1ConnectionChildChannel( + channel: serverTestConnectionChannel, + asyncChannelConfiguration: .init() + ).get() + + // Write the connection channel to the server channel to simulate an incoming connection + try await self.serverTestChannel.writeInbound(serverAsyncConnectionChannel) + + // Now, we could write requests directly to `serverAsyncConnectionChannel`, but it expects `ByteBuffer` + // inputs. This is cumbersome to work with in tests, so we create a client channel, and use it to send + // requests and observe responses in terms of HTTP types. + let (clientTestChannel, clientAsyncChannel) = try await self.setUpClientConnection() + + try await withThrowingDiscardingTaskGroup { group in + // We must forward all client outbound writes to the server and vice-versa. + group.addTask { try await clientTestChannel.glueTo(serverTestConnectionChannel) } + + try await body(clientAsyncChannel) + + try await serverTestConnectionChannel.close() + } + } + + /// Creates a client testing channel configured with HTTP channel handlers and wraps it in a `NIOAsyncChannel`. + private func setUpClientConnection() async throws -> ( + NIOAsyncTestingChannel, + NIOAsyncChannel + ) { + let clientTestChannel = try await NIOAsyncTestingChannel { channel in + try NIOHTTP1Client.clientChannelInitializer(channel) + } + + // Wrap the client channel in a NIOAsyncChannel for convenience + let clientAsyncChannel = try await clientTestChannel.eventLoop.submit { + try NIOAsyncChannel( + wrappingChannelSynchronously: clientTestChannel, + configuration: .init(isOutboundHalfClosureEnabled: true) + ) + }.get() + + return (clientTestChannel, clientAsyncChannel) + } + } +} diff --git a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+SecureUpgrade.swift b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelClient+SecureUpgrade.swift similarity index 65% rename from Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+SecureUpgrade.swift rename to Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelClient+SecureUpgrade.swift index c3b6059..956baa3 100644 --- a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+SecureUpgrade.swift +++ b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelClient+SecureUpgrade.swift @@ -12,67 +12,19 @@ // //===----------------------------------------------------------------------===// -import HTTPServer -import Logging import NIOCore import NIOEmbedded import NIOHTTP2 import NIOHTTPTypes -import NIOHTTPTypesHTTP1 -import NIOHTTPTypesHTTP2 import NIOSSL import X509 @testable import NIOHTTPServer @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) -extension TestingChannelServer { - /// Sets up the server with a testing channel and the provided request handler, starts the server, and provides - /// `Self` to the `body` closure. Call `withConnectedClient(clientTLSConfiguration:body:)` on the provided instance - /// to simulate incoming connections. - static func withSecureUpgradeClient( - logger: Logger, - tlsConfiguration: TLSConfiguration, - tlsVerificationCallback: (@Sendable ([Certificate]) async throws -> CertificateVerificationResult)? = nil, - http2Configuration: NIOHTTP2Handler.Configuration = .init(), - handler: some HTTPServerRequestHandler, - body: (SecureUpgradeClient) async throws -> Void - ) async throws { - let server = NIOHTTPServer( - logger: logger, - // The server won't actually be bound to this host and port; we just have to pass this argument - configuration: .init(bindTarget: .hostAndPort(host: "127.0.0.1", port: 8000)) - ) - - // Create a test channel. We will run the server on this channel. - let serverTestChannel = NIOAsyncTestingChannel() - - try await withThrowingTaskGroup { group in - // We are ready now. Start the server with the test channel. - group.addTask { - try await server.serveSecureUpgradeWithTestChannel(testChannel: serverTestChannel, handler: handler) - } - - // Execute the provided closure with a `SecureUpgradeClient` instance - try await body( - SecureUpgradeClient( - server: server, - serverTestChannel: serverTestChannel, - serverTLSConfiguration: tlsConfiguration, - verificationCallback: tlsVerificationCallback, - http2Configuration: http2Configuration - ) - ) - - group.cancelAll() - } - } -} - -@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) -extension TestingChannelServer { +extension TestingChannelSecureUpgradeServer { // A Secure Upgrade HTTP client backed by a `NIOAsyncTestingChannel`. - struct SecureUpgradeClient { + struct Client { let server: NIOHTTPServer let serverTestChannel: NIOAsyncTestingChannel @@ -88,7 +40,7 @@ extension TestingChannelServer { body: (NegotiatedClientConnection) async throws -> Void ) async throws { // Create a connection channel: we will write this to the server channel to simulate an incoming connection. - let serverTestConnectionChannel = try await TestingChannelServer.createServerConnectionChannel() + let serverTestConnectionChannel = try await NIOAsyncTestingChannel.createActiveChannel() // Set up the required channel handlers on `serverTestConnectionChannel` let negotiatedServerConnectionFuture = try await serverTestConnectionChannel.eventLoop.flatSubmit { diff --git a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+HTTP1.swift b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+HTTP1.swift new file mode 100644 index 0000000..f5dc358 --- /dev/null +++ b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+HTTP1.swift @@ -0,0 +1,66 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP Server open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import HTTPServer +import Logging +import NIOEmbedded +import NIOHTTPServer + +/// A testing utility that sets up a `NIOHTTPServer` instance based on `NIOAsyncTestingChannel` (instead of the +/// `ServerSocketChannel` that `NIOHTTPServer` normally uses) and vends a client instance for sending requests and +/// observing responses. +/// +/// This provider creates a `NIOHTTPServer` instance using a `NIOAsyncTestingChannel` as its listening channel. Since no +/// network socket is actually listening for incoming connections, client connections are simulated by *writing* a +/// connection channel to the server channel. This connection channel is set up with the same handlers that +/// `ServerBootstrap` would set up and vend to `NIOHTTPServer` on an incoming connection. +/// +/// This provider vends a HTTP client channel (also backed by a `NIOAsyncTestingChannel`) that can be used to send +/// requests and observe responses in terms of HTTP types (`HTTPRequestPart` and `HTTPResponsePart`) to the server +/// connection channel. +@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) +struct TestingChannelHTTP1Server { + /// Creates a `NIOHTTPServer` backed by a `NIOAsyncTestingChannel` and the provided request handler, starts it, and + /// provides a `HTTP1Client` to the `body` closure. + static func withClient( + logger: Logger, + serverRequestHandler: some HTTPServerRequestHandler< + HTTPRequestConcludingAsyncReader, HTTPResponseConcludingAsyncWriter + >, + body: (Client) async throws -> Void + ) async throws { + let server = NIOHTTPServer( + logger: logger, + // The server won't actually be bound to this host and port; we just have to pass this argument. + configuration: .init(bindTarget: .hostAndPort(host: "127.0.0.1", port: 8000)) + ) + // Create a test channel. We will run the server on this channel. + let serverTestChannel = NIOAsyncTestingChannel() + + try await withThrowingTaskGroup { group in + // We are ready now. Start the server with the test channel. + group.addTask { + try await server.serveInsecureHTTP1_1WithTestChannel( + testChannel: serverTestChannel, + handler: serverRequestHandler + ) + } + + // Execute the provided closure with a test client instance. + try await body(Client(server: server, serverTestChannel: serverTestChannel)) + + group.cancelAll() + } + } +} diff --git a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift new file mode 100644 index 0000000..06c3a13 --- /dev/null +++ b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP Server open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import HTTPServer +import Logging +import NIOEmbedded +import NIOHTTP2 +import NIOSSL +import X509 + +@testable import NIOHTTPServer + +/// Like ``TestingChannelHTTP1Server``, but for Secure Upgrade. +@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) +struct TestingChannelSecureUpgradeServer { + /// Sets up the server with a testing channel and the provided request handler, starts the server, and provides + /// `Self` to the `body` closure. Call `withConnection(clientTLSConfiguration:body:)` on the provided instance to + /// simulate incoming connections. + static func withClient( + logger: Logger, + tlsConfiguration: TLSConfiguration, + tlsVerificationCallback: (@Sendable ([Certificate]) async throws -> CertificateVerificationResult)? = nil, + http2Configuration: NIOHTTP2Handler.Configuration = .init(), + handler: some HTTPServerRequestHandler, + body: (Client) async throws -> Void + ) async throws { + let server = NIOHTTPServer( + logger: logger, + // The server won't actually be bound to this host and port; we just have to pass this argument + configuration: .init(bindTarget: .hostAndPort(host: "127.0.0.1", port: 8000)) + ) + + // Create a test channel. We will run the server on this channel. + let serverTestChannel = NIOAsyncTestingChannel() + + try await withThrowingTaskGroup { group in + // We are ready now. Start the server with the test channel. + group.addTask { + try await server.serveSecureUpgradeWithTestChannel(testChannel: serverTestChannel, handler: handler) + } + + // Execute the provided closure with a test client instance. + try await body( + Client( + server: server, + serverTestChannel: serverTestChannel, + serverTLSConfiguration: tlsConfiguration, + verificationCallback: tlsVerificationCallback, + http2Configuration: http2Configuration + ) + ) + + group.cancelAll() + } + } +} diff --git a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+HTTP1.swift b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+HTTP1.swift deleted file mode 100644 index 466f96d..0000000 --- a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingClientServerProvider+HTTP1.swift +++ /dev/null @@ -1,151 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP Server open source project -// -// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import HTTPServer -import Logging -import NIOCore -import NIOEmbedded -import NIOHTTP1 -import NIOHTTPTypes -import NIOHTTPTypesHTTP1 -import X509 - -@testable import NIOHTTPServer - -/// A testing utility that sets up a `NIOHTTPServer` instance based on `NIOAsyncTestingChannel` (instead of the -/// `ServerSocketChannel` that `NIOHTTPServer` normally uses) and vends a client instance for sending requests and -/// observing responses. -/// -/// This provider creates a `NIOHTTPServer` instance using a `NIOAsyncTestingChannel` as its listening channel. Since no -/// network socket is actually listening for incoming connections, client connections are simulated by *writing* a -/// connection channel to the server channel. This connection channel is set up with the same handlers that -/// `ServerBootstrap` would set up and vend to `NIOHTTPServer` on an incoming connection. -/// -/// This provider vends a HTTP client channel (also backed by a `NIOAsyncTestingChannel`) that can be used to send -/// requests and observe responses in terms of HTTP types (`HTTPRequestPart` and `HTTPResponsePart`) to the server -/// connection channel. -@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) -struct TestingChannelServer { - /// Creates a `NIOHTTPServer` backed by a `NIOAsyncTestingChannel` and the provided request handler, starts it, and - /// provides a `HTTP1Client` to the `body` closure. - static func withPlaintextHTTP1Client( - logger: Logger, - serverRequestHandler: some HTTPServerRequestHandler< - HTTPRequestConcludingAsyncReader, HTTPResponseConcludingAsyncWriter - >, - body: (PlaintextHTTP1Client) async throws -> Void - ) async throws { - let server = NIOHTTPServer( - logger: logger, - // The server won't actually be bound to this host and port; we just have to pass this argument. - configuration: .init(bindTarget: .hostAndPort(host: "127.0.0.1", port: 8000)) - ) - // Create a test channel. We will run the server on this channel. - let serverTestChannel = NIOAsyncTestingChannel() - - try await withThrowingTaskGroup { group in - // We are ready now. Start the server with the test channel. - group.addTask { - try await server.serveInsecureHTTP1_1WithTestChannel( - testChannel: serverTestChannel, - handler: serverRequestHandler - ) - } - - // Execute the provided closure with a `PlaintextHTTP1Client` instance created from the server instance and - // the test channel instance. - try await body(PlaintextHTTP1Client(server: server, serverTestChannel: serverTestChannel)) - - group.cancelAll() - } - } - - /// Creates a connection channel that can be written to the server channel to simulate an incoming connection. - static func createServerConnectionChannel() async throws -> NIOAsyncTestingChannel { - let serverTestConnectionChannel = NIOAsyncTestingChannel() - - let connectionPromise = serverTestConnectionChannel.eventLoop.makePromise(of: Void.self) - // The `to` address has no significance here, it is just a random address. We are just interested in making the - // channel "active"; calling `connect` is the way to achieve that. - serverTestConnectionChannel.connect( - to: try .init(ipAddress: "127.0.0.1", port: 8000), - promise: connectionPromise - ) - try await connectionPromise.futureResult.get() - - return serverTestConnectionChannel - } -} - -@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) -extension TestingChannelServer { - /// A plaintext HTTP/1.1 client backed by a `NIOAsyncTestingChannel`. - struct PlaintextHTTP1Client { - let server: NIOHTTPServer - let serverTestChannel: NIOAsyncTestingChannel - - /// Starts a new connection with the server and executes the provided `body` closure. - /// - Parameter body: A closure that should send a request using the provided client channel and validate the - /// received response. - func withConnection( - body: (NIOAsyncChannel) async throws -> Void - ) async throws { - // Create a connection channel: we will write this to the server channel to simulate an incoming connection - let serverTestConnectionChannel = try await TestingChannelServer.createServerConnectionChannel() - - // Set up the required channel handlers on `serverTestConnectionChannel` - let serverAsyncConnectionChannel = try await self.server.setupHTTP1_1ConnectionChildChannel( - channel: serverTestConnectionChannel, - asyncChannelConfiguration: .init() - ).get() - - // Write the connection channel to the server channel to simulate an incoming connection - try await self.serverTestChannel.writeInbound(serverAsyncConnectionChannel) - - // Now, we could write requests directly to `serverAsyncConnectionChannel`, but it expects `ByteBuffer` - // inputs. This is cumbersome to work with in tests, so we create a client channel, and use it to send - // requests and observe responses in terms of HTTP types. - let (clientTestChannel, clientAsyncChannel) = try await self.setUpClientConnection() - - try await withThrowingDiscardingTaskGroup { group in - // We must forward all client outbound writes to the server and vice-versa. - group.addTask { try await clientTestChannel.glueTo(serverTestConnectionChannel) } - - try await body(clientAsyncChannel) - - try await serverTestConnectionChannel.close() - } - } - - /// Creates a client testing channel configured with HTTP channel handlers and wraps it in a `NIOAsyncChannel`. - private func setUpClientConnection() async throws -> ( - NIOAsyncTestingChannel, - NIOAsyncChannel - ) { - let clientTestChannel = try await NIOAsyncTestingChannel { channel in - try NIOHTTP1Client.clientChannelInitializer(channel) - } - - // Wrap the client channel in a NIOAsyncChannel for convenience - let clientAsyncChannel = try await clientTestChannel.eventLoop.submit { - try NIOAsyncChannel( - wrappingChannelSynchronously: clientTestChannel, - configuration: .init(isOutboundHalfClosureEnabled: true) - ) - }.get() - - return (clientTestChannel, clientAsyncChannel) - } - } -} From a37be4a74fbb9db65a0364dc0fbf31d7e20293f8 Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Thu, 12 Feb 2026 09:58:13 +0000 Subject: [PATCH 04/13] Update `createActiveChannel` --- Tests/NIOHTTPServerTests/Utilities/Helpers.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/NIOHTTPServerTests/Utilities/Helpers.swift b/Tests/NIOHTTPServerTests/Utilities/Helpers.swift index 0bd80db..eb74d07 100644 --- a/Tests/NIOHTTPServerTests/Utilities/Helpers.swift +++ b/Tests/NIOHTTPServerTests/Utilities/Helpers.swift @@ -37,19 +37,19 @@ extension NIOAsyncTestingChannel { } } - /// Creates a connection channel that can be written to the server channel to simulate an incoming connection. + /// Returns a `NIOAsyncTestingChannel` that is set to the `active` state. static func createActiveChannel() async throws -> NIOAsyncTestingChannel { - let serverTestConnectionChannel = NIOAsyncTestingChannel() + let channel = NIOAsyncTestingChannel() - let connectionPromise = serverTestConnectionChannel.eventLoop.makePromise(of: Void.self) - // The `to` address has no significance here, it is just a random address. We are only interested in making the + let setToActivePromise = channel.eventLoop.makePromise(of: Void.self) + // The `to` address has no significance here: it is just a random address. We are only interested in making the // channel *active*; calling `connect` is the way to achieve that. - serverTestConnectionChannel.connect( + channel.connect( to: try .init(ipAddress: "127.0.0.1", port: 8000), - promise: connectionPromise + promise: setToActivePromise ) - try await connectionPromise.futureResult.get() + try await setToActivePromise.futureResult.get() - return serverTestConnectionChannel + return channel } } From 95c1c9b91b23d59bf408c59c8022c55ae63c58c8 Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Wed, 18 Feb 2026 10:50:51 +0000 Subject: [PATCH 05/13] Restructure NIO-based client channel setup methods to extensions under `Channel` and `ClientBootstrap` --- .../NIOHTTPServerTests.swift | 18 +- .../Utilities/NIOClient/NIOClient+HTTP1.swift | 48 +++--- .../NIOClient/NIOClient+SecureUpgrade.swift | 158 +++++++++--------- 3 files changed, 114 insertions(+), 110 deletions(-) diff --git a/Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift b/Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift index 1f847a3..3702afb 100644 --- a/Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift +++ b/Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift @@ -17,6 +17,7 @@ import HTTPTypes import Logging import NIOCore import NIOHTTPTypes +import NIOPosix import Testing import X509 @@ -106,7 +107,9 @@ struct NIOHTTPServerTests { } }, body: { serverAddress in - let client = try await NIOHTTP1Client.setUpChannel(at: serverAddress) + let client = try await ClientBootstrap(group: .singletonMultiThreadedEventLoopGroup) + .connectToTestHTTP1Server(at: serverAddress) + try await client.executeThenClose { inbound, outbound in try await outbound.write(Self.reqHead) try await outbound.write(Self.reqBody) @@ -183,12 +186,13 @@ struct NIOHTTPServerTests { } }, body: { serverAddress in - let client = try await NIOSecureUpgradeClient.setUpMTLSChannel( - at: serverAddress, - clientChain: clientChain, - trustRoots: [serverChain.ca], - applicationProtocol: applicationProtocol - ) + let client = try await ClientBootstrap(group: .singletonMultiThreadedEventLoopGroup) + .connectToTestSecureUpgradeHTTPServerOverMTLS( + at: serverAddress, + clientChain: clientChain, + trustRoots: [serverChain.ca], + applicationProtocol: applicationProtocol + ) let clientChannel: NIOAsyncChannel switch client { diff --git a/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+HTTP1.swift b/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+HTTP1.swift index 210a6ab..70b43db 100644 --- a/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+HTTP1.swift +++ b/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+HTTP1.swift @@ -18,34 +18,32 @@ import NIOHTTPTypes import NIOHTTPTypesHTTP1 import NIOPosix -/// Provides NIO HTTP clients for testing that one can interact with using `NIOHTTPTypes`. -/// -/// Server responses are `NIOHTTPTypes/HTTPResponsePart` and client requests are `NIOHTTPTypes/HTTPRequestPart`. -/// With the ``NIOAsyncChannel`` the `setUpClient` methods vend, one can write `HTTPRequestPart`s to the channel -/// and observe `HTTPResponsePart`s from the inbound stream of the async channel. @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) -struct NIOHTTP1Client { - /// Configures a channel with HTTP/1.1 client handlers for interaction in terms of HTTP types. - static func clientChannelInitializer(_ channel: Channel) throws { - try channel.pipeline.syncOperations.addHTTPClientHandlers() - try channel.pipeline.syncOperations.addHandler(HTTP1ToHTTPClientCodec()) +extension Channel { + /// Adds HTTP/1.1 client handlers to the pipeline. + func configureTestHTTP1ClientPipeline() -> EventLoopFuture> { + self.eventLoop.makeCompletedFuture { + try self.pipeline.syncOperations.addHTTPClientHandlers() + try self.pipeline.syncOperations.addHandler(HTTP1ToHTTPClientCodec()) + + return try NIOAsyncChannel( + wrappingChannelSynchronously: self, + configuration: .init(isOutboundHalfClosureEnabled: true) + ) + } } +} - /// Creates and connects an HTTP/1.1 client to the specified address. - static func setUpChannel( - at address: NIOHTTPServer.SocketAddress +@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) +extension ClientBootstrap { + /// Connects to the provided `serverAddress` and provides a `NIOAsyncChannel`. With this ``NIOAsyncChannel``, one + /// can write `HTTPRequestPart`s to the server and observe `HTTPResponsePart`s from the inbound stream of the + /// channel. + func connectToTestHTTP1Server( + at serverAddress: NIOHTTPServer.SocketAddress ) async throws -> NIOAsyncChannel { - try await ClientBootstrap(group: .singletonMultiThreadedEventLoopGroup) - .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) - .connect(to: try .init(ipAddress: address.host, port: address.port)) { channel in - channel.eventLoop.makeCompletedFuture { - try self.clientChannelInitializer(channel) - - return try NIOAsyncChannel( - wrappingChannelSynchronously: channel, - configuration: .init(isOutboundHalfClosureEnabled: true) - ) - } - } + try await self.connect(to: try .init(ipAddress: serverAddress.host, port: serverAddress.port)) { channel in + channel.configureTestHTTP1ClientPipeline() + } } } diff --git a/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+SecureUpgrade.swift b/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+SecureUpgrade.swift index f9db21b..10fc935 100644 --- a/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+SecureUpgrade.swift +++ b/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+SecureUpgrade.swift @@ -23,57 +23,50 @@ import X509 @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) /// Provides a HTTP client with ALPN negotiation. -struct NIOSecureUpgradeClient { - /// Valid `applicationProtocol` values are `"http/1.1"` (forces HTTP/1.1), `"h2"` (forces HTTP/2), or a - /// comma-separated combination of both in order of preference, e.g. `"http/1.1, h2"`. - static func setUpTLSConfig(trustRoots: [Certificate], applicationProtocol: String) throws -> TLSConfiguration { - var clientTLSConfig = TLSConfiguration.makeClientConfiguration() - clientTLSConfig.trustRoots = .certificates(try trustRoots.map { try NIOSSLCertificate($0) }) - clientTLSConfig.certificateVerification = .noHostnameVerification - clientTLSConfig.applicationProtocols = [applicationProtocol] - - return clientTLSConfig - } - - /// Creates and connects a TLS-enabled client to the specified address with ALPN negotiation. - static func setUpChannel( - at address: NIOHTTPServer.SocketAddress, - trustRoots: [Certificate], - applicationProtocol: String - ) async throws -> NegotiatedClientConnection { - let tlsConfig = try self.setUpTLSConfig(trustRoots: trustRoots, applicationProtocol: applicationProtocol) - - return try await self._setUpChannel(at: address, tlsConfig: tlsConfig) +extension Channel { + /// Adds a ``NIOSSLClientHandler`` configured with the provided `TLSConfiguration` to the pipeline. + func configureTestClientSSLPipeline(tlsConfig: TLSConfiguration) -> EventLoopFuture { + self.eventLoop.makeCompletedFuture { + let sslContext = try NIOSSLContext(configuration: tlsConfig) + let sslHandler = try NIOSSLClientHandler(context: sslContext, serverHostname: nil) + try self.pipeline.syncOperations.addHandler(sslHandler) + } } - /// Exactly like ``setUpClient(at:trustRoots:applicationProtocol:)`` but with mTLS enabled. - static func setUpMTLSChannel( - at address: NIOHTTPServer.SocketAddress, - clientChain: ChainPrivateKeyPair, - trustRoots: [Certificate], - applicationProtocol: String, - ) async throws -> NegotiatedClientConnection { - var tlsConfig = try self.setUpTLSConfig(trustRoots: trustRoots, applicationProtocol: applicationProtocol) - tlsConfig.certificateChain = [try NIOSSLCertificateSource(clientChain.leaf)] - tlsConfig.privateKey = .privateKey(try .init(clientChain.privateKey)) - - return try await self._setUpChannel(at: address, tlsConfig: tlsConfig) + /// Adds an ALPN handler (configured with both HTTP/1.1 and HTTP/2 channel initializers) to the pipeline. + func configureTestSecureUpgradeClientPipeline() -> EventLoopFuture< + EventLoopFuture< + NIONegotiatedHTTPVersion< + NIOAsyncChannel, + NIOHTTP2Handler.AsyncStreamMultiplexer + > + > + > { + self.configureHTTP2AsyncSecureUpgrade( + http1ConnectionInitializer: { channel in + channel.configureTestHTTP1ClientPipeline() + }, + http2ConnectionInitializer: { channel in + channel.configureAsyncHTTP2Pipeline(mode: .client) { $0.eventLoop.makeSucceededFuture($0) } + } + ) } +} - /// Creates and connects a client to the specified address with the provided TLS configuration. - private static func _setUpChannel( - at address: NIOHTTPServer.SocketAddress, +@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) +extension ClientBootstrap { + /// Connects the client to the specified address using the provided TLS configuration. + func connectToTestSecureUpgradeHTTPServer( + at serverAddress: NIOHTTPServer.SocketAddress, tlsConfig: TLSConfiguration ) async throws -> NegotiatedClientConnection { - let clientNegotiatedChannel = try await ClientBootstrap(group: .singletonMultiThreadedEventLoopGroup) - .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) - .connect(to: try .init(ipAddress: address.host, port: address.port)) { channel in - channel.eventLoop.makeCompletedFuture { - try self.sslClientChannelInitializer(channel, tlsConfig: tlsConfig) - }.flatMap { - self.clientChannelInitializer(channel, tlsConfig: tlsConfig) - } - }.get() + let clientNegotiatedChannel = try await self.connect( + to: try .init(ipAddress: serverAddress.host, port: serverAddress.port) + ) { channel in + channel.configureTestClientSSLPipeline(tlsConfig: tlsConfig).flatMap { + channel.configureTestSecureUpgradeClientPipeline() + } + }.get() switch clientNegotiatedChannel { case .http1_1(let http1Channel): @@ -84,42 +77,51 @@ struct NIOSecureUpgradeClient { } } - /// Sets up the input child channel with a ``NIOSSLClientHandler`` configured with the provided TLS - /// configuration. - static func sslClientChannelInitializer(_ channel: Channel, tlsConfig: TLSConfiguration) throws { - let sslContext = try NIOSSLContext(configuration: tlsConfig) + /// Creates and connects a TLS-enabled client to the specified address. + func connectToTestSecureUpgradeHTTPServer( + at serverAddress: NIOHTTPServer.SocketAddress, + trustRoots: [Certificate], + applicationProtocol: String + ) async throws -> NegotiatedClientConnection { + let tlsConfig = try TLSConfiguration.makeTestClientConfiguration( + testTrustRoots: trustRoots, + applicationProtocol: applicationProtocol + ) - let sslHandler = try NIOSSLClientHandler(context: sslContext, serverHostname: nil) - try channel.pipeline.syncOperations.addHandler(sslHandler) + return try await self.connectToTestSecureUpgradeHTTPServer(at: serverAddress, tlsConfig: tlsConfig) } - /// Provides channel initializers for HTTP/1.1 and HTTP/2 to the ALPN handler, which selects one based on - /// the negotiated result. - static func clientChannelInitializer( - _ channel: Channel, - tlsConfig: TLSConfiguration - ) -> EventLoopFuture< - EventLoopFuture< - NIONegotiatedHTTPVersion< - NIOAsyncChannel, - NIOHTTP2Handler.AsyncStreamMultiplexer - > - > - > { - channel.configureHTTP2AsyncSecureUpgrade( - http1ConnectionInitializer: { channel in - channel.eventLoop.makeCompletedFuture { - try NIOHTTP1Client.clientChannelInitializer(channel) - - return try NIOAsyncChannel( - wrappingChannelSynchronously: channel, - configuration: .init(isOutboundHalfClosureEnabled: true) - ) - } - }, - http2ConnectionInitializer: { channel in - channel.configureAsyncHTTP2Pipeline(mode: .client) { $0.eventLoop.makeSucceededFuture($0) } - } + /// Exactly like ``connectToTestSecureUpgradeHTTPServerOverMTLS(at:trustRoots:applicationProtocol:)`` but over mTLS + /// instead. + func connectToTestSecureUpgradeHTTPServerOverMTLS( + at serverAddress: NIOHTTPServer.SocketAddress, + clientChain: ChainPrivateKeyPair, + trustRoots: [Certificate], + applicationProtocol: String + ) async throws -> NegotiatedClientConnection { + var mTLSConfig = try TLSConfiguration.makeTestClientConfiguration( + testTrustRoots: trustRoots, + applicationProtocol: applicationProtocol ) + mTLSConfig.certificateChain = [try NIOSSLCertificateSource(clientChain.leaf)] + mTLSConfig.privateKey = .privateKey(try .init(clientChain.privateKey)) + + return try await self.connectToTestSecureUpgradeHTTPServer(at: serverAddress, tlsConfig: mTLSConfig) + } +} + +extension TLSConfiguration { + /// Valid `applicationProtocol` values are `"http/1.1"` (forces HTTP/1.1), `"h2"` (forces HTTP/2), or a + /// comma-separated combination of both in order of preference, e.g. `"http/1.1, h2"`. + static func makeTestClientConfiguration( + testTrustRoots: [Certificate], + applicationProtocol: String + ) throws -> TLSConfiguration { + var clientTLSConfig = TLSConfiguration.makeClientConfiguration() + clientTLSConfig.trustRoots = .certificates(try testTrustRoots.map { try NIOSSLCertificate($0) }) + clientTLSConfig.certificateVerification = .noHostnameVerification + clientTLSConfig.applicationProtocols = [applicationProtocol] + + return clientTLSConfig } } From f25b18c81d9cc5842b673a20de0e6eb93489b6bc Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Wed, 18 Feb 2026 10:57:58 +0000 Subject: [PATCH 06/13] Remove `Client` type from `TestingChannelServer` --- .../TestingChannelClient+HTTP1.swift | 81 -------------- .../TestingChannelClient+SecureUpgrade.swift | 104 ------------------ 2 files changed, 185 deletions(-) delete mode 100644 Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelClient+HTTP1.swift delete mode 100644 Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelClient+SecureUpgrade.swift diff --git a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelClient+HTTP1.swift b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelClient+HTTP1.swift deleted file mode 100644 index 57e6a6d..0000000 --- a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelClient+HTTP1.swift +++ /dev/null @@ -1,81 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP Server open source project -// -// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import NIOCore -import NIOEmbedded -import NIOHTTPTypes - -@testable import NIOHTTPServer - -@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) -extension TestingChannelHTTP1Server { - /// A plaintext HTTP/1.1 client backed by a `NIOAsyncTestingChannel`. - struct Client { - let server: NIOHTTPServer - let serverTestChannel: NIOAsyncTestingChannel - - /// Starts a new connection with the server and executes the provided `body` closure. - /// - Parameter body: A closure that should send a request using the provided client channel and validate the - /// received response. - func withConnection( - body: (NIOAsyncChannel) async throws -> Void - ) async throws { - // Create a connection channel: we will write this to the server channel to simulate an incoming connection - let serverTestConnectionChannel = try await NIOAsyncTestingChannel.createActiveChannel() - - // Set up the required channel handlers on `serverTestConnectionChannel` - let serverAsyncConnectionChannel = try await self.server.setupHTTP1_1ConnectionChildChannel( - channel: serverTestConnectionChannel, - asyncChannelConfiguration: .init() - ).get() - - // Write the connection channel to the server channel to simulate an incoming connection - try await self.serverTestChannel.writeInbound(serverAsyncConnectionChannel) - - // Now, we could write requests directly to `serverAsyncConnectionChannel`, but it expects `ByteBuffer` - // inputs. This is cumbersome to work with in tests, so we create a client channel, and use it to send - // requests and observe responses in terms of HTTP types. - let (clientTestChannel, clientAsyncChannel) = try await self.setUpClientConnection() - - try await withThrowingDiscardingTaskGroup { group in - // We must forward all client outbound writes to the server and vice-versa. - group.addTask { try await clientTestChannel.glueTo(serverTestConnectionChannel) } - - try await body(clientAsyncChannel) - - try await serverTestConnectionChannel.close() - } - } - - /// Creates a client testing channel configured with HTTP channel handlers and wraps it in a `NIOAsyncChannel`. - private func setUpClientConnection() async throws -> ( - NIOAsyncTestingChannel, - NIOAsyncChannel - ) { - let clientTestChannel = try await NIOAsyncTestingChannel { channel in - try NIOHTTP1Client.clientChannelInitializer(channel) - } - - // Wrap the client channel in a NIOAsyncChannel for convenience - let clientAsyncChannel = try await clientTestChannel.eventLoop.submit { - try NIOAsyncChannel( - wrappingChannelSynchronously: clientTestChannel, - configuration: .init(isOutboundHalfClosureEnabled: true) - ) - }.get() - - return (clientTestChannel, clientAsyncChannel) - } - } -} diff --git a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelClient+SecureUpgrade.swift b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelClient+SecureUpgrade.swift deleted file mode 100644 index 956baa3..0000000 --- a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelClient+SecureUpgrade.swift +++ /dev/null @@ -1,104 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP Server open source project -// -// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import NIOCore -import NIOEmbedded -import NIOHTTP2 -import NIOHTTPTypes -import NIOSSL -import X509 - -@testable import NIOHTTPServer - -@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) -extension TestingChannelSecureUpgradeServer { - // A Secure Upgrade HTTP client backed by a `NIOAsyncTestingChannel`. - struct Client { - let server: NIOHTTPServer - let serverTestChannel: NIOAsyncTestingChannel - - let serverTLSConfiguration: TLSConfiguration - let verificationCallback: (@Sendable ([Certificate]) async throws -> CertificateVerificationResult)? - - let http2Configuration: NIOHTTP2Handler.Configuration - - /// Starts a new TLS connection with ALPN negotiation to the server and executes the provided `body` closure - /// with the negotiated ALPN result as an argument. - func withConnection( - tlsConfig: TLSConfiguration, - body: (NegotiatedClientConnection) async throws -> Void - ) async throws { - // Create a connection channel: we will write this to the server channel to simulate an incoming connection. - let serverTestConnectionChannel = try await NIOAsyncTestingChannel.createActiveChannel() - - // Set up the required channel handlers on `serverTestConnectionChannel` - let negotiatedServerConnectionFuture = try await serverTestConnectionChannel.eventLoop.flatSubmit { - self.server.setupSecureUpgradeConnectionChildChannel( - channel: serverTestConnectionChannel, - tlsConfiguration: self.serverTLSConfiguration, - asyncChannelConfiguration: .init(), - http2Configuration: self.http2Configuration, - verificationCallback: self.verificationCallback - ) - }.get() - - // Write the connection channel to the server channel to simulate an incoming connection - try await self.serverTestChannel.writeInbound(negotiatedServerConnectionFuture) - - // Now, we could write requests directly to `serverAsyncConnectionChannel`, but it expects `ByteBuffer` - // inputs. This is cumbersome to work with in tests, so we create a client channel, and use it to send - // requests and observe responses in terms of HTTP types. - let (clientTestChannel, clientNegotiatedConnectionFuture) = try await self.setUpClientConnection( - tlsConfig: tlsConfig - ) - - try await withThrowingDiscardingTaskGroup { group in - // We must forward all client outbound writes to the server and vice-versa. - group.addTask { try await clientTestChannel.glueTo(serverTestConnectionChannel) } - - try await body(.init(negotiationResult: try await clientNegotiatedConnectionFuture.get())) - - try await serverTestConnectionChannel.close() - } - } - - private func setUpClientConnection( - tlsConfig: TLSConfiguration - ) async throws -> ( - NIOAsyncTestingChannel, - EventLoopFuture< - NIONegotiatedHTTPVersion< - NIOAsyncChannel, NIOHTTP2Handler.AsyncStreamMultiplexer - > - > - ) { - let clientTestChannel = try await NIOAsyncTestingChannel { channel in - _ = channel.eventLoop.makeCompletedFuture { - try NIOSecureUpgradeClient.sslClientChannelInitializer(channel, tlsConfig: tlsConfig) - } - } - - let clientNegotiatedConnection = try await NIOSecureUpgradeClient.clientChannelInitializer( - clientTestChannel, - tlsConfig: tlsConfig - ).get() - - let connectionPromise = clientTestChannel.eventLoop.makePromise(of: Void.self) - clientTestChannel.connect(to: try .init(ipAddress: "127.0.0.1", port: 8000), promise: connectionPromise) - try await connectionPromise.futureResult.get() - - return (clientTestChannel, clientNegotiatedConnection) - } - } -} From 1e843210d848f90bab672d9f22361df47e66efe9 Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Wed, 18 Feb 2026 11:08:59 +0000 Subject: [PATCH 07/13] Add `withConnectedClient` instance method under `TestingChannelServer` --- .../NIOHTTPServerEndToEndTests.swift | 14 ++--- .../TestingChannelServer+HTTP1.swift | 60 ++++++++++++++---- .../TestingChannelServer+SecureUpgrade.swift | 61 +++++++++++++++++-- 3 files changed, 109 insertions(+), 26 deletions(-) diff --git a/Tests/NIOHTTPServerTests/NIOHTTPServerEndToEndTests.swift b/Tests/NIOHTTPServerTests/NIOHTTPServerEndToEndTests.swift index 950eb45..a60eea1 100644 --- a/Tests/NIOHTTPServerTests/NIOHTTPServerEndToEndTests.swift +++ b/Tests/NIOHTTPServerTests/NIOHTTPServerEndToEndTests.swift @@ -32,9 +32,9 @@ struct NIOHTTPServerEndToEndTests { @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) @Test("HTTP/1.1 request and response") func testHTTP1_1() async throws { - try await TestingChannelHTTP1Server.withClient( + try await TestingChannelHTTP1Server.serve( logger: Logger(label: "NIOHTTPServerEndToEndTests"), - serverRequestHandler: HTTPServerClosureRequestHandler { request, reqContext, reqReader, resSender in + handler: HTTPServerClosureRequestHandler { request, reqContext, reqReader, resSender in let sender = try await resSender.send(.init(status: .ok)) try await sender.produceAndConclude { writer in @@ -43,8 +43,8 @@ struct NIOHTTPServerEndToEndTests { return [.serverTiming: "test"] } } - ) { client in - try await client.withConnection { connectionChannel in + ) { server in + try await server.withConnectedClient { connectionChannel in try await connectionChannel.executeThenClose { inbound, outbound in try await outbound.write(.head(.init(method: .get, scheme: "", authority: "", path: "/"))) try await outbound.write(.end(nil)) @@ -92,7 +92,7 @@ struct NIOHTTPServerEndToEndTests { clientTLSConfig.certificateVerification = .noHostnameVerification clientTLSConfig.applicationProtocols = ["h2"] - try await TestingChannelSecureUpgradeServer.withClient( + try await TestingChannelSecureUpgradeServer.serve( logger: Logger(label: "NIOHTTPServerEndToEndTests"), tlsConfiguration: serverTLSConfig, handler: HTTPServerClosureRequestHandler { request, reqContext, reqReader, resSender in @@ -104,8 +104,8 @@ struct NIOHTTPServerEndToEndTests { return [.serverTiming: "test"] } } - ) { client in - try await client.withConnection(tlsConfig: clientTLSConfig) { negotiatedConnectionChannel in + ) { server in + try await server.withConnectedClient(clientTLSConfig: clientTLSConfig) { negotiatedConnectionChannel in switch negotiatedConnectionChannel { case .http1(_): Issue.record("Failed to negotiate HTTP/2 despite the client requiring HTTP/2.") diff --git a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+HTTP1.swift b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+HTTP1.swift index f5dc358..6e68cad 100644 --- a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+HTTP1.swift +++ b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+HTTP1.swift @@ -14,8 +14,11 @@ import HTTPServer import Logging +import NIOCore import NIOEmbedded -import NIOHTTPServer +import NIOHTTPTypes + +@testable import NIOHTTPServer /// A testing utility that sets up a `NIOHTTPServer` instance based on `NIOAsyncTestingChannel` (instead of the /// `ServerSocketChannel` that `NIOHTTPServer` normally uses) and vends a client instance for sending requests and @@ -31,14 +34,15 @@ import NIOHTTPServer /// connection channel. @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) struct TestingChannelHTTP1Server { + let server: NIOHTTPServer + let serverTestChannel: NIOAsyncTestingChannel + /// Creates a `NIOHTTPServer` backed by a `NIOAsyncTestingChannel` and the provided request handler, starts it, and - /// provides a `HTTP1Client` to the `body` closure. - static func withClient( + /// provides `Self` to the `body` closure. + static func serve( logger: Logger, - serverRequestHandler: some HTTPServerRequestHandler< - HTTPRequestConcludingAsyncReader, HTTPResponseConcludingAsyncWriter - >, - body: (Client) async throws -> Void + handler: some HTTPServerRequestHandler, + body: (Self) async throws -> Void ) async throws { let server = NIOHTTPServer( logger: logger, @@ -51,16 +55,46 @@ struct TestingChannelHTTP1Server { try await withThrowingTaskGroup { group in // We are ready now. Start the server with the test channel. group.addTask { - try await server.serveInsecureHTTP1_1WithTestChannel( - testChannel: serverTestChannel, - handler: serverRequestHandler - ) + try await server.serveInsecureHTTP1_1WithTestChannel(testChannel: serverTestChannel, handler: handler) } - // Execute the provided closure with a test client instance. - try await body(Client(server: server, serverTestChannel: serverTestChannel)) + // Execute the provided closure with `Self`. + try await body(Self(server: server, serverTestChannel: serverTestChannel)) group.cancelAll() } } + + /// Starts a new connection with the server and executes the provided `body` closure. + /// - Parameter body: A closure that should send a request using the provided client channel and validate the + /// received response. + func withConnectedClient( + body: (_ connectionChannel: NIOAsyncChannel) async throws -> Void + ) async throws { + // Create a connection channel: we will write this to the server channel to simulate an incoming connection + let serverTestConnectionChannel = try await NIOAsyncTestingChannel.createActiveChannel() + + // Set up the required channel handlers on `serverTestConnectionChannel` + let serverAsyncConnectionChannel = try await self.server.setupHTTP1_1ConnectionChildChannel( + channel: serverTestConnectionChannel, + asyncChannelConfiguration: .init() + ).get() + + // Write the connection channel to the server channel to simulate an incoming connection + try await self.serverTestChannel.writeInbound(serverAsyncConnectionChannel) + + let clientTestingChannel = try await NIOAsyncTestingChannel.createActiveChannel() + let clientAsyncChannel = try await clientTestingChannel.eventLoop.flatSubmit { + clientTestingChannel.configureTestHTTP1ClientPipeline() + }.get() + + try await withThrowingDiscardingTaskGroup { group in + // We must forward all client outbound writes to the server and vice-versa. + group.addTask { try await clientTestingChannel.glueTo(serverTestConnectionChannel) } + + try await body(clientAsyncChannel) + + try await serverTestConnectionChannel.close() + } + } } diff --git a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift index 06c3a13..b2f614a 100644 --- a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift +++ b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift @@ -14,8 +14,10 @@ import HTTPServer import Logging +import NIOCore import NIOEmbedded import NIOHTTP2 +import NIOHTTPTypes import NIOSSL import X509 @@ -24,16 +26,23 @@ import X509 /// Like ``TestingChannelHTTP1Server``, but for Secure Upgrade. @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) struct TestingChannelSecureUpgradeServer { + let server: NIOHTTPServer + let serverTestChannel: NIOAsyncTestingChannel + + let tlsConfiguration: TLSConfiguration + let tlsVerificationCallback: (@Sendable ([Certificate]) async throws -> CertificateVerificationResult)? + let http2Configuration: NIOHTTP2Handler.Configuration + /// Sets up the server with a testing channel and the provided request handler, starts the server, and provides /// `Self` to the `body` closure. Call `withConnection(clientTLSConfiguration:body:)` on the provided instance to /// simulate incoming connections. - static func withClient( + static func serve( logger: Logger, tlsConfiguration: TLSConfiguration, tlsVerificationCallback: (@Sendable ([Certificate]) async throws -> CertificateVerificationResult)? = nil, http2Configuration: NIOHTTP2Handler.Configuration = .init(), handler: some HTTPServerRequestHandler, - body: (Client) async throws -> Void + body: (Self) async throws -> Void ) async throws { let server = NIOHTTPServer( logger: logger, @@ -50,13 +59,13 @@ struct TestingChannelSecureUpgradeServer { try await server.serveSecureUpgradeWithTestChannel(testChannel: serverTestChannel, handler: handler) } - // Execute the provided closure with a test client instance. + // Execute the provided closure. try await body( - Client( + Self( server: server, serverTestChannel: serverTestChannel, - serverTLSConfiguration: tlsConfiguration, - verificationCallback: tlsVerificationCallback, + tlsConfiguration: tlsConfiguration, + tlsVerificationCallback: tlsVerificationCallback, http2Configuration: http2Configuration ) ) @@ -64,4 +73,44 @@ struct TestingChannelSecureUpgradeServer { group.cancelAll() } } + + /// Starts a new TLS connection with ALPN negotiation to the server and executes the provided `body` closure + /// with the negotiated ALPN result as an argument. + func withConnectedClient( + clientTLSConfig: TLSConfiguration, + body: (_ negotiatedConnectionChannel: NegotiatedClientConnection) async throws -> Void + ) async throws { + // Create a connection channel: we will write this to the server channel to simulate an incoming connection. + let serverTestConnectionChannel = try await NIOAsyncTestingChannel.createActiveChannel() + + // Set up the required channel handlers on `serverTestConnectionChannel` + let negotiatedServerConnectionFuture = try await serverTestConnectionChannel.eventLoop.flatSubmit { + self.server.setupSecureUpgradeConnectionChildChannel( + channel: serverTestConnectionChannel, + tlsConfiguration: self.tlsConfiguration, + asyncChannelConfiguration: .init(), + http2Configuration: self.http2Configuration, + verificationCallback: self.tlsVerificationCallback + ) + }.get() + + // Write the connection channel to the server channel to simulate an incoming connection + try await self.serverTestChannel.writeInbound(negotiatedServerConnectionFuture) + + let clientTestingChannel = try await NIOAsyncTestingChannel.createActiveChannel() + let clientNegotiatedConnectionFuture = try await clientTestingChannel.eventLoop.flatSubmit { + clientTestingChannel.configureTestClientSSLPipeline(tlsConfig: clientTLSConfig).flatMap { + clientTestingChannel.configureTestSecureUpgradeClientPipeline() + } + }.get() + + try await withThrowingDiscardingTaskGroup { group in + // We must forward all client outbound writes to the server and vice-versa. + group.addTask { try await clientTestingChannel.glueTo(serverTestConnectionChannel) } + + try await body(.init(negotiationResult: try await clientNegotiatedConnectionFuture.get())) + + try await serverTestConnectionChannel.close() + } + } } From 8003870e44db34a46936318dc7a0c03d57569d27 Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Wed, 18 Feb 2026 13:05:10 +0000 Subject: [PATCH 08/13] Use `withThrowingDiscardingTaskGroup` --- .../NIOHTTPServerTests/Utilities/Helpers.swift | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Tests/NIOHTTPServerTests/Utilities/Helpers.swift b/Tests/NIOHTTPServerTests/Utilities/Helpers.swift index eb74d07..5b11453 100644 --- a/Tests/NIOHTTPServerTests/Utilities/Helpers.swift +++ b/Tests/NIOHTTPServerTests/Utilities/Helpers.swift @@ -18,20 +18,28 @@ import NIOEmbedded extension NIOAsyncTestingChannel { /// Forwards all of our outbound writes to `other` and vice-versa. func glueTo(_ other: NIOAsyncTestingChannel) async throws { - await withThrowingTaskGroup { group in + try await withThrowingDiscardingTaskGroup { group in // 1. Forward all `self` writes to `other` group.addTask { while !Task.isCancelled { - let ourPart = try await self.waitForOutboundWrite(as: ByteBuffer.self) - try await other.writeInbound(ourPart) + do { + let ourPart = try await self.waitForOutboundWrite(as: ByteBuffer.self) + try await other.writeInbound(ourPart) + } catch ChannelError.ioOnClosedChannel { + return + } } } // 2. Forward all `other` writes to `self` group.addTask { while !Task.isCancelled { - let otherPart = try await other.waitForOutboundWrite(as: ByteBuffer.self) - try await self.writeInbound(otherPart) + do { + let otherPart = try await other.waitForOutboundWrite(as: ByteBuffer.self) + try await self.writeInbound(otherPart) + } catch ChannelError.ioOnClosedChannel { + return + } } } } From 672644cbf150a45dc563878ffdfd1f7c043f06eb Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Thu, 19 Feb 2026 10:30:36 +0000 Subject: [PATCH 09/13] Update availability --- Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift | 2 +- .../Utilities/NIOClient/NIOClient+HTTP1.swift | 4 ++-- .../Utilities/NIOClient/NIOClient+SecureUpgrade.swift | 4 ++-- .../TestingChannelServer+HTTP1.swift | 2 +- .../TestingChannelServer+SecureUpgrade.swift | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift b/Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift index bda6053..a7fa03d 100644 --- a/Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift +++ b/Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift @@ -230,7 +230,7 @@ struct NIOHTTPServerTests { } } -@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) extension NIOHTTPServerTests { static func withServer( server: NIOHTTPServer, diff --git a/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+HTTP1.swift b/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+HTTP1.swift index 70b43db..99bb74f 100644 --- a/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+HTTP1.swift +++ b/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+HTTP1.swift @@ -18,7 +18,7 @@ import NIOHTTPTypes import NIOHTTPTypesHTTP1 import NIOPosix -@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) extension Channel { /// Adds HTTP/1.1 client handlers to the pipeline. func configureTestHTTP1ClientPipeline() -> EventLoopFuture> { @@ -34,7 +34,7 @@ extension Channel { } } -@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) extension ClientBootstrap { /// Connects to the provided `serverAddress` and provides a `NIOAsyncChannel`. With this ``NIOAsyncChannel``, one /// can write `HTTPRequestPart`s to the server and observe `HTTPResponsePart`s from the inbound stream of the diff --git a/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+SecureUpgrade.swift b/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+SecureUpgrade.swift index 10fc935..6631a0d 100644 --- a/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+SecureUpgrade.swift +++ b/Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+SecureUpgrade.swift @@ -21,7 +21,7 @@ import X509 @testable import NIOHTTPServer -@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) /// Provides a HTTP client with ALPN negotiation. extension Channel { /// Adds a ``NIOSSLClientHandler`` configured with the provided `TLSConfiguration` to the pipeline. @@ -53,7 +53,7 @@ extension Channel { } } -@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) extension ClientBootstrap { /// Connects the client to the specified address using the provided TLS configuration. func connectToTestSecureUpgradeHTTPServer( diff --git a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+HTTP1.swift b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+HTTP1.swift index 6e68cad..a79ee41 100644 --- a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+HTTP1.swift +++ b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+HTTP1.swift @@ -32,7 +32,7 @@ import NIOHTTPTypes /// This provider vends a HTTP client channel (also backed by a `NIOAsyncTestingChannel`) that can be used to send /// requests and observe responses in terms of HTTP types (`HTTPRequestPart` and `HTTPResponsePart`) to the server /// connection channel. -@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) struct TestingChannelHTTP1Server { let server: NIOHTTPServer let serverTestChannel: NIOAsyncTestingChannel diff --git a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift index b2f614a..ff0d76f 100644 --- a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift +++ b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift @@ -24,7 +24,7 @@ import X509 @testable import NIOHTTPServer /// Like ``TestingChannelHTTP1Server``, but for Secure Upgrade. -@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) struct TestingChannelSecureUpgradeServer { let server: NIOHTTPServer let serverTestChannel: NIOAsyncTestingChannel From 67c2df409e403bbafd0784c8705060272ccad558 Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Thu, 19 Feb 2026 10:31:16 +0000 Subject: [PATCH 10/13] Resolve references to `NIOHTTPServer`'s underlying reader and writer types --- Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift b/Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift index a7fa03d..a78fd76 100644 --- a/Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift +++ b/Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift @@ -234,7 +234,10 @@ struct NIOHTTPServerTests { extension NIOHTTPServerTests { static func withServer( server: NIOHTTPServer, - serverHandler: some HTTPServerRequestHandler, + serverHandler: some HTTPServerRequestHandler< + NIOHTTPServer.RequestConcludingReader, + NIOHTTPServer.ResponseConcludingWriter + >, body: (NIOHTTPServer.SocketAddress) async throws -> Void ) async throws { try await withThrowingTaskGroup { group in From 78f3aa2c5b7a2b72a7fb24c60c4786e105eba950 Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Mon, 23 Feb 2026 20:29:57 +0000 Subject: [PATCH 11/13] Add comment about ChannelError.ioOnClosedChannel handling --- Tests/NIOHTTPServerTests/Utilities/Helpers.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tests/NIOHTTPServerTests/Utilities/Helpers.swift b/Tests/NIOHTTPServerTests/Utilities/Helpers.swift index 5b11453..3a33e42 100644 --- a/Tests/NIOHTTPServerTests/Utilities/Helpers.swift +++ b/Tests/NIOHTTPServerTests/Utilities/Helpers.swift @@ -26,6 +26,9 @@ extension NIOAsyncTestingChannel { let ourPart = try await self.waitForOutboundWrite(as: ByteBuffer.self) try await other.writeInbound(ourPart) } catch ChannelError.ioOnClosedChannel { + // We only reach here if the channel has closed. `waitForOutboundWrite` uses a continuation + // without `withTaskCancellationHandler`, so this error is the only shutdown signal; returning + // allows the task group and `glueTo` to complete cleanly. return } } @@ -38,6 +41,8 @@ extension NIOAsyncTestingChannel { let otherPart = try await other.waitForOutboundWrite(as: ByteBuffer.self) try await self.writeInbound(otherPart) } catch ChannelError.ioOnClosedChannel { + // Same reasoning as above: the channel has closed, and returning allows the task group and + // `glueTo` to complete cleanly. return } } From ed30d1329d29809dc666329ccf0eef3b8c67529d Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Wed, 25 Feb 2026 16:06:03 +0000 Subject: [PATCH 12/13] Fix tests --- .../NIOHTTPServer+ServiceLifecycleTests.swift | 65 +++++++++++-------- .../TestingChannelServer+SecureUpgrade.swift | 4 +- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/Tests/NIOHTTPServerTests/NIOHTTPServer+ServiceLifecycleTests.swift b/Tests/NIOHTTPServerTests/NIOHTTPServer+ServiceLifecycleTests.swift index 8e95e9e..efb3ed0 100644 --- a/Tests/NIOHTTPServerTests/NIOHTTPServer+ServiceLifecycleTests.swift +++ b/Tests/NIOHTTPServerTests/NIOHTTPServer+ServiceLifecycleTests.swift @@ -79,7 +79,8 @@ struct NIOHTTPServiceLifecycleTests { let serverAddress = try await server.listeningAddress - let client = try await setUpClient(host: serverAddress.host, port: serverAddress.port) + let client = try await ClientBootstrap(group: .singletonMultiThreadedEventLoopGroup) + .connectToTestHTTP1Server(at: serverAddress) try await client.executeThenClose { inbound, outbound in try await outbound.write(Self.reqHead) @@ -160,7 +161,8 @@ struct NIOHTTPServiceLifecycleTests { let serverAddress = try await server.listeningAddress - let client = try await setUpClient(host: serverAddress.host, port: serverAddress.port) + let client = try await ClientBootstrap(group: .singletonMultiThreadedEventLoopGroup) + .connectToTestHTTP1Server(at: serverAddress) try await client.executeThenClose { inbound, outbound in try await outbound.write(Self.reqHead) @@ -235,34 +237,41 @@ struct NIOHTTPServiceLifecycleTests { let serverAddress = try await server.listeningAddress - let client = try await setUpClientWithMTLS( - at: serverAddress, - chain: clientChain, - trustRoots: [serverChain.ca], - applicationProtocol: "h2" - ) - - try await client.executeThenClose { inbound, outbound in - try await outbound.write(Self.reqHead) - try await outbound.write(Self.reqBody) - - // Wait until the server has received the request. - try await firstChunkReadPromise.futureResult.get() - - // Now trigger graceful shutdown. This should propagate down to the server. The server will - // start the 500ms grace timer after which all connections that are still open will be - // forcefully closed. - trigger.triggerGracefulShutdown() - - // The server should shut down after 500ms. Wait for this. - try await group.waitForAll() + let client = try await ClientBootstrap(group: .singletonMultiThreadedEventLoopGroup) + .connectToTestSecureUpgradeHTTPServer( + at: serverAddress, + trustRoots: [serverChain.ca], + applicationProtocol: "h2" + ) + + switch client { + case .http1: + Issue.record("Unexpectedly negotiated a HTTP/2 connection") + + case .http2(let streamManager): + let streamChannel = try await streamManager.openStream() + try await streamChannel.executeThenClose { inbound, outbound in + try await outbound.write(Self.reqHead) + try await outbound.write(Self.reqBody) + + // Wait until the server has received the request. + try await firstChunkReadPromise.futureResult.get() + + // Now trigger graceful shutdown. This should propagate down to the server. The server will + // start the 500ms grace timer after which all connections that are still open will be + // forcefully closed. + trigger.triggerGracefulShutdown() + + // The server should shut down after 500ms. Wait for this. + try await group.waitForAll() + + // The connection should have been closed: we should get an `ioOnClosedChannel` error. + await #expect(throws: ChannelError.ioOnClosedChannel) { + try await outbound.write(Self.reqEnd) + } - // The connection should have been closed: we should get an `ioOnClosedChannel` error. - await #expect(throws: ChannelError.ioOnClosedChannel) { - try await outbound.write(Self.reqEnd) + connectionForcefullyShutdown() } - - connectionForcefullyShutdown() } } } diff --git a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift index ff0d76f..ac623d2 100644 --- a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift +++ b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift @@ -31,7 +31,7 @@ struct TestingChannelSecureUpgradeServer { let tlsConfiguration: TLSConfiguration let tlsVerificationCallback: (@Sendable ([Certificate]) async throws -> CertificateVerificationResult)? - let http2Configuration: NIOHTTP2Handler.Configuration + let http2Configuration: NIOHTTPServerConfiguration.HTTP2 /// Sets up the server with a testing channel and the provided request handler, starts the server, and provides /// `Self` to the `body` closure. Call `withConnection(clientTLSConfiguration:body:)` on the provided instance to @@ -40,7 +40,7 @@ struct TestingChannelSecureUpgradeServer { logger: Logger, tlsConfiguration: TLSConfiguration, tlsVerificationCallback: (@Sendable ([Certificate]) async throws -> CertificateVerificationResult)? = nil, - http2Configuration: NIOHTTP2Handler.Configuration = .init(), + http2Configuration: NIOHTTPServerConfiguration.HTTP2 = .init(), handler: some HTTPServerRequestHandler, body: (Self) async throws -> Void ) async throws { From d0605c19b24a013f1f3186b45430bb65f5950ac9 Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Thu, 26 Feb 2026 13:35:02 +0000 Subject: [PATCH 13/13] Fix warning --- .../NIOHTTPServerTests/NIOHTTPServer+ServiceLifecycleTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/NIOHTTPServerTests/NIOHTTPServer+ServiceLifecycleTests.swift b/Tests/NIOHTTPServerTests/NIOHTTPServer+ServiceLifecycleTests.swift index efb3ed0..3799db3 100644 --- a/Tests/NIOHTTPServerTests/NIOHTTPServer+ServiceLifecycleTests.swift +++ b/Tests/NIOHTTPServerTests/NIOHTTPServer+ServiceLifecycleTests.swift @@ -193,7 +193,6 @@ struct NIOHTTPServiceLifecycleTests { @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) func testActiveHTTP2ConnectionIsShutDownAfterGraceTimeout() async throws { let serverChain = try TestCA.makeSelfSignedChain() - let clientChain = try TestCA.makeSelfSignedChain() let server = NIOHTTPServer( logger: self.serverLogger,