Pārlūkot izejas kodu

Add `NIOTransportServices` H2 client transport (#2054)

Add a `NIOTransportServices` H2 client transport to gRPC v2.
Gus Cairo 1 gadu atpakaļ
vecāks
revīzija
6b5b54ec34

+ 5 - 5
Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift

@@ -25,7 +25,7 @@ extension HTTP2ClientTransport {
 }
 
 extension HTTP2ClientTransport.Config {
-  public struct Compression: Sendable {
+  public struct Compression: Sendable, Hashable {
     /// The default algorithm used for compressing outbound messages.
     ///
     /// This can be overridden on a per-call basis via `CallOptions`.
@@ -51,7 +51,7 @@ extension HTTP2ClientTransport.Config {
   }
 
   @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
-  public struct Keepalive: Sendable {
+  public struct Keepalive: Sendable, Hashable {
     /// The amount of time to wait after reading data before sending a keepalive ping.
     ///
     /// - Note: The transport may choose to increase this value if it is less than 10 seconds.
@@ -73,7 +73,7 @@ extension HTTP2ClientTransport.Config {
   }
 
   @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
-  public struct Connection: Sendable {
+  public struct Connection: Sendable, Hashable {
     /// The maximum amount of time a connection may be idle before it's closed.
     ///
     /// Connections are considered idle when there are no open streams on them. Idle connections
@@ -103,7 +103,7 @@ extension HTTP2ClientTransport.Config {
   }
 
   @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
-  public struct Backoff: Sendable {
+  public struct Backoff: Sendable, Hashable {
     /// The initial duration to wait before reattempting to establish a connection.
     public var initial: Duration
 
@@ -135,7 +135,7 @@ extension HTTP2ClientTransport.Config {
     }
   }
 
-  public struct HTTP2: Sendable {
+  public struct HTTP2: Sendable, Hashable {
     /// The max frame size, in bytes.
     ///
     /// The actual value used is clamped to `(1 << 14) ... (1 << 24) - 1` (the min and max values

+ 17 - 0
Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift

@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+private import GRPCCore
 package import NIOCore
 
 extension GRPCHTTP2Core.SocketAddress {
@@ -38,6 +39,22 @@ extension GRPCHTTP2Core.SocketAddress {
 }
 
 extension NIOCore.SocketAddress {
+  package init(_ socketAddress: GRPCHTTP2Core.SocketAddress) throws {
+    if let ipv4 = socketAddress.ipv4 {
+      self = try Self(ipv4)
+    } else if let ipv6 = socketAddress.ipv6 {
+      self = try Self(ipv6)
+    } else if let unixDomainSocket = socketAddress.unixDomainSocket {
+      self = try Self(unixDomainSocket)
+    } else {
+      throw RPCError(
+        code: .internalError,
+        message:
+          "Unsupported mapping to NIOCore/SocketAddress for GRPCHTTP2Core/SocketAddress: \(socketAddress)."
+      )
+    }
+  }
+
   package init(_ address: GRPCHTTP2Core.SocketAddress.IPv4) throws {
     try self.init(ipAddress: address.host, port: address.port)
   }

+ 6 - 6
Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift

@@ -26,7 +26,7 @@ extension HTTP2ServerTransport {
 }
 
 extension HTTP2ServerTransport.Config {
-  public struct Compression: Sendable {
+  public struct Compression: Sendable, Hashable {
     /// Compression algorithms enabled for inbound messages.
     ///
     /// - Note: `CompressionAlgorithm.none` is always supported, even if it isn't set here.
@@ -46,7 +46,7 @@ extension HTTP2ServerTransport.Config {
   }
 
   @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
-  public struct Keepalive: Sendable {
+  public struct Keepalive: Sendable, Hashable {
     /// The amount of time to wait after reading data before sending a keepalive ping.
     public var time: Duration
 
@@ -80,7 +80,7 @@ extension HTTP2ServerTransport.Config {
   }
 
   @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
-  public struct ClientKeepaliveBehavior: Sendable {
+  public struct ClientKeepaliveBehavior: Sendable, Hashable {
     /// The minimum allowed interval the client is allowed to send keep-alive pings.
     /// Pings more frequent than this interval count as 'strikes' and the connection is closed if there are
     /// too many strikes.
@@ -107,7 +107,7 @@ extension HTTP2ServerTransport.Config {
   }
 
   @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
-  public struct Connection: Sendable {
+  public struct Connection: Sendable, Hashable {
     /// The maximum amount of time a connection may exist before being gracefully closed.
     public var maxAge: Duration?
 
@@ -142,7 +142,7 @@ extension HTTP2ServerTransport.Config {
     }
   }
 
-  public struct HTTP2: Sendable {
+  public struct HTTP2: Sendable, Hashable {
     /// The maximum frame size to be used in an HTTP/2 connection.
     public var maxFrameSize: Int
 
@@ -175,7 +175,7 @@ extension HTTP2ServerTransport.Config {
     }
   }
 
-  public struct RPC: Sendable {
+  public struct RPC: Sendable, Hashable {
     /// The maximum request payload size.
     public var maxRequestPayloadSize: Int
 

+ 4 - 5
Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift

@@ -16,11 +16,11 @@
 
 public import GRPCCore
 public import GRPCHTTP2Core  // should be @usableFromInline
-public import NIOCore
+public import NIOCore  // has to be public because of EventLoopGroup param in init
 public import NIOPosix  // has to be public because of default argument value in init
 
 #if canImport(NIOSSL)
-import NIOSSL
+private import NIOSSL
 #endif
 
 @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
@@ -28,7 +28,7 @@ extension HTTP2ClientTransport {
   /// A ``GRPCCore/ClientTransport`` using HTTP/2 built on top of `NIOPosix`.
   ///
   /// This transport builds on top of SwiftNIO's Posix networking layer and is suitable for use
-  /// on Linux and Darwin based platform (macOS, iOS, etc.) However, it's *strongly* recommended
+  /// on Linux and Darwin based platforms (macOS, iOS, etc.). However, it's *strongly* recommended
   /// that if you are targeting Darwin platforms then you should use the `NIOTS` variant of
   /// the ``GRPCHTTP2Core/HTTP2ClientTransport``.
   ///
@@ -62,7 +62,7 @@ extension HTTP2ClientTransport {
   public struct Posix: ClientTransport {
     private let channel: GRPCChannel
 
-    /// Creates a new Posix based HTTP/2 client transport.
+    /// Creates a new NIOPosix-based HTTP/2 client transport.
     ///
     /// - Parameters:
     ///   - target: A target to resolve.
@@ -91,7 +91,6 @@ extension HTTP2ClientTransport {
         )
       }
 
-      // Configure a connector.
       self.channel = GRPCChannel(
         resolver: resolver,
         connector: try Connector(eventLoopGroup: eventLoopGroup, config: config),

+ 0 - 18
Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift

@@ -425,24 +425,6 @@ extension HTTP2ServerTransport.Posix {
   }
 }
 
-extension NIOCore.SocketAddress {
-  fileprivate init(_ socketAddress: GRPCHTTP2Core.SocketAddress) throws {
-    if let ipv4 = socketAddress.ipv4 {
-      self = try Self(ipv4)
-    } else if let ipv6 = socketAddress.ipv6 {
-      self = try Self(ipv6)
-    } else if let unixDomainSocket = socketAddress.unixDomainSocket {
-      self = try Self(unixDomainSocket)
-    } else {
-      throw RPCError(
-        code: .internalError,
-        message:
-          "Unsupported mapping to NIOCore/SocketAddress for GRPCHTTP2Core/SocketAddress: \(socketAddress)."
-      )
-    }
-  }
-}
-
 extension ServerBootstrap {
   @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
   fileprivate func bind<Output: Sendable>(

+ 339 - 0
Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ClientTransport+TransportServices.swift

@@ -0,0 +1,339 @@
+/*
+ * Copyright 2024, gRPC Authors All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#if canImport(Network)
+public import GRPCCore
+public import GRPCHTTP2Core
+public import NIOTransportServices  // has to be public because of default argument value in init
+public import NIOCore  // has to be public because of EventLoopGroup param in init
+
+private import Network
+
+@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
+extension HTTP2ClientTransport {
+  /// A ``GRPCCore/ClientTransport`` using HTTP/2 built on top of `NIOTransportServices`.
+  ///
+  /// This transport builds on top of SwiftNIO's Transport Services networking layer and is the recommended
+  /// variant for use on Darwin-based platforms (macOS, iOS, etc.).
+  /// If you are targeting Linux platforms then you should use the `NIOPosix` variant of
+  /// the ``GRPCHTTP2Core/HTTP2ClientTransport``.
+  ///
+  /// To use this transport you need to provide a 'target' to connect to which will be resolved
+  /// by an appropriate resolver from the resolver registry. By default the resolver registry can
+  /// resolve DNS targets, IPv4 and IPv6 targets, and Unix domain socket targets. Virtual Socket
+  /// targets are not supported with this transport. If you use a custom target you must also provide an
+  /// appropriately configured registry.
+  ///
+  /// You can control various aspects of connection creation, management, security and RPC behavior via
+  /// the ``Config``. Load balancing policies and other RPC specific behavior can be configured via
+  /// the ``ServiceConfig`` (if it isn't provided by a resolver).
+  ///
+  /// Beyond creating the transport you don't need to interact with it directly, instead, pass it
+  /// to a `GRPCClient`:
+  ///
+  /// ```swift
+  /// try await withThrowingDiscardingTaskGroup { group in
+  ///   let transport = try HTTP2ClientTransport.TransportServices(
+  ///     target: .ipv4(host: "example.com"),
+  ///     config: .defaults(transportSecurity: .plaintext)
+  ///   )
+  ///   let client = GRPCClient(transport: transport)
+  ///   group.addTask {
+  ///     try await client.run()
+  ///   }
+  ///
+  ///   // ...
+  /// }
+  /// ```
+  public struct TransportServices: ClientTransport {
+    private let channel: GRPCChannel
+
+    public var retryThrottle: RetryThrottle? {
+      self.channel.retryThrottle
+    }
+
+    /// Creates a new NIOTransportServices-based HTTP/2 client transport.
+    ///
+    /// - Parameters:
+    ///   - target: A target to resolve.
+    ///   - config: Configuration for the transport.
+    ///   - resolverRegistry: A registry of resolver factories.
+    ///   - serviceConfig: Service config controlling how the transport should establish and
+    ///       load-balance connections.
+    ///   - eventLoopGroup: The underlying NIO `EventLoopGroup` to run connections on. This must
+    ///       be a `MultiThreadedEventLoopGroup` or an `EventLoop` from
+    ///       a `MultiThreadedEventLoopGroup`.
+    /// - Throws: When no suitable resolver could be found for the `target`.
+    public init(
+      target: any ResolvableTarget,
+      config: Config,
+      resolverRegistry: NameResolverRegistry = .defaults,
+      serviceConfig: ServiceConfig = ServiceConfig(),
+      eventLoopGroup: any EventLoopGroup = .singletonNIOTSEventLoopGroup
+    ) throws {
+      guard let resolver = resolverRegistry.makeResolver(for: target) else {
+        throw RuntimeError(
+          code: .transportError,
+          message: """
+            No suitable resolvers to resolve '\(target)'. You must make sure that the resolver \
+            registry has a suitable name resolver factory registered for the given target.
+            """
+        )
+      }
+
+      self.channel = GRPCChannel(
+        resolver: resolver,
+        connector: Connector(eventLoopGroup: eventLoopGroup, config: config),
+        config: GRPCChannel.Config(transportServices: config),
+        defaultServiceConfig: serviceConfig
+      )
+    }
+
+    public func connect() async throws {
+      await self.channel.connect()
+    }
+
+    public func beginGracefulShutdown() {
+      self.channel.beginGracefulShutdown()
+    }
+
+    public func withStream<T: Sendable>(
+      descriptor: MethodDescriptor,
+      options: CallOptions,
+      _ closure: (RPCStream<Inbound, Outbound>) async throws -> T
+    ) async throws -> T {
+      try await self.channel.withStream(descriptor: descriptor, options: options, closure)
+    }
+
+    public func configuration(forMethod descriptor: MethodDescriptor) -> MethodConfig? {
+      self.channel.configuration(forMethod: descriptor)
+    }
+  }
+}
+
+@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
+extension HTTP2ClientTransport.TransportServices {
+  struct Connector: HTTP2Connector {
+    private let config: HTTP2ClientTransport.TransportServices.Config
+    private let eventLoopGroup: any EventLoopGroup
+
+    init(
+      eventLoopGroup: any EventLoopGroup,
+      config: HTTP2ClientTransport.TransportServices.Config
+    ) {
+      self.eventLoopGroup = eventLoopGroup
+      self.config = config
+    }
+
+    func establishConnection(
+      to address: GRPCHTTP2Core.SocketAddress
+    ) async throws -> HTTP2Connection {
+      let bootstrap: NIOTSConnectionBootstrap
+      let isPlainText: Bool
+      switch self.config.transportSecurity.wrapped {
+      case .plaintext:
+        isPlainText = true
+        bootstrap = NIOTSConnectionBootstrap(group: self.eventLoopGroup)
+
+      case .tls(let tlsConfig):
+        isPlainText = false
+        bootstrap = NIOTSConnectionBootstrap(group: self.eventLoopGroup)
+          .tlsOptions(try NWProtocolTLS.Options(tlsConfig))
+      }
+
+      let (channel, multiplexer) = try await bootstrap.connect(to: address) { channel in
+        channel.eventLoop.makeCompletedFuture {
+          try channel.pipeline.syncOperations.configureGRPCClientPipeline(
+            channel: channel,
+            config: GRPCChannel.Config(transportServices: self.config)
+          )
+        }
+      }
+
+      return HTTP2Connection(
+        channel: channel,
+        multiplexer: multiplexer,
+        isPlaintext: isPlainText
+      )
+    }
+  }
+}
+
+@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
+extension HTTP2ClientTransport.TransportServices {
+  /// Configuration for the `TransportServices` transport.
+  public struct Config: Sendable {
+    /// Configuration for HTTP/2 connections.
+    public var http2: HTTP2ClientTransport.Config.HTTP2
+
+    /// Configuration for backoff used when establishing a connection.
+    public var backoff: HTTP2ClientTransport.Config.Backoff
+
+    /// Configuration for connection management.
+    public var connection: HTTP2ClientTransport.Config.Connection
+
+    /// Compression configuration.
+    public var compression: HTTP2ClientTransport.Config.Compression
+
+    /// The transport's security.
+    public var transportSecurity: TransportSecurity
+
+    /// Creates a new connection configuration.
+    ///
+    /// - Parameters:
+    ///   - http2: HTTP2 configuration.
+    ///   - backoff: Backoff configuration.
+    ///   - connection: Connection configuration.
+    ///   - compression: Compression configuration.
+    ///   - transportSecurity: The transport's security configuration.
+    ///
+    /// - SeeAlso: ``defaults(_:)``.
+    public init(
+      http2: HTTP2ClientTransport.Config.HTTP2,
+      backoff: HTTP2ClientTransport.Config.Backoff,
+      connection: HTTP2ClientTransport.Config.Connection,
+      compression: HTTP2ClientTransport.Config.Compression,
+      transportSecurity: TransportSecurity
+    ) {
+      self.http2 = http2
+      self.connection = connection
+      self.backoff = backoff
+      self.compression = compression
+      self.transportSecurity = transportSecurity
+    }
+
+    /// Default values.
+    ///
+    /// - Parameters:
+    ///   - transportSecurity: The security settings applied to the transport.
+    ///   - configure: A closure which allows you to modify the defaults before returning them.
+    public static func defaults(
+      transportSecurity: TransportSecurity,
+      configure: (_ config: inout Self) -> Void = { _ in }
+    ) -> Self {
+      var config = Self(
+        http2: .defaults,
+        backoff: .defaults,
+        connection: .defaults,
+        compression: .defaults,
+        transportSecurity: transportSecurity
+      )
+      configure(&config)
+      return config
+    }
+  }
+}
+
+@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
+extension GRPCChannel.Config {
+  init(transportServices config: HTTP2ClientTransport.TransportServices.Config) {
+    self.init(
+      http2: config.http2,
+      backoff: config.backoff,
+      connection: config.connection,
+      compression: config.compression
+    )
+  }
+}
+
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+extension NIOTSConnectionBootstrap {
+  fileprivate func connect<Output: Sendable>(
+    to address: GRPCHTTP2Core.SocketAddress,
+    childChannelInitializer: @escaping @Sendable (any Channel) -> EventLoopFuture<Output>
+  ) async throws -> Output {
+    if address.virtualSocket != nil {
+      throw RuntimeError(
+        code: .transportError,
+        message: """
+            Virtual sockets are not supported by 'HTTP2ClientTransport.TransportServices'. \
+            Please use the 'HTTP2ClientTransport.Posix' transport.
+          """
+      )
+    } else {
+      return try await self.connect(
+        to: NIOCore.SocketAddress(address),
+        channelInitializer: childChannelInitializer
+      )
+    }
+  }
+}
+
+@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
+extension ClientTransport where Self == HTTP2ClientTransport.TransportServices {
+  /// Create a new `TransportServices` based HTTP/2 client transport.
+  ///
+  /// - Parameters:
+  ///   - target: A target to resolve.
+  ///   - config: Configuration for the transport.
+  ///   - resolverRegistry: A registry of resolver factories.
+  ///   - serviceConfig: Service config controlling how the transport should establish and
+  ///       load-balance connections.
+  ///   - eventLoopGroup: The underlying NIO `EventLoopGroup` to run connections on. This must
+  ///       be a `NIOTSEventLoopGroup` or an `EventLoop` from
+  ///       a `NIOTSEventLoopGroup`.
+  /// - Throws: When no suitable resolver could be found for the `target`.
+  public static func http2NIOTS(
+    target: any ResolvableTarget,
+    config: HTTP2ClientTransport.TransportServices.Config,
+    resolverRegistry: NameResolverRegistry = .defaults,
+    serviceConfig: ServiceConfig = ServiceConfig(),
+    eventLoopGroup: any EventLoopGroup = .singletonNIOTSEventLoopGroup
+  ) throws -> Self {
+    try HTTP2ClientTransport.TransportServices(
+      target: target,
+      config: config,
+      resolverRegistry: resolverRegistry,
+      serviceConfig: serviceConfig,
+      eventLoopGroup: eventLoopGroup
+    )
+  }
+}
+
+@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
+extension NWProtocolTLS.Options {
+  convenience init(_ tlsConfig: HTTP2ClientTransport.TransportServices.Config.TLS) throws {
+    self.init()
+
+    guard let sec_identity = sec_identity_create(try tlsConfig.identityProvider()) else {
+      throw RuntimeError(
+        code: .transportError,
+        message: """
+          There was an issue creating the SecIdentity required to set up TLS. \
+          Please check your TLS configuration.
+          """
+      )
+    }
+
+    sec_protocol_options_set_local_identity(
+      self.securityProtocolOptions,
+      sec_identity
+    )
+
+    sec_protocol_options_set_min_tls_protocol_version(
+      self.securityProtocolOptions,
+      .TLSv12
+    )
+
+    for `protocol` in ["grpc-exp", "h2"] {
+      sec_protocol_options_add_tls_application_protocol(
+        self.securityProtocolOptions,
+        `protocol`
+      )
+    }
+  }
+}
+#endif

+ 0 - 18
Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift

@@ -367,24 +367,6 @@ extension HTTP2ServerTransport.TransportServices {
   }
 }
 
-extension NIOCore.SocketAddress {
-  fileprivate init(_ socketAddress: GRPCHTTP2Core.SocketAddress) throws {
-    if let ipv4 = socketAddress.ipv4 {
-      self = try Self(ipv4)
-    } else if let ipv6 = socketAddress.ipv6 {
-      self = try Self(ipv6)
-    } else if let unixDomainSocket = socketAddress.unixDomainSocket {
-      self = try Self(unixDomainSocket)
-    } else {
-      throw RPCError(
-        code: .internalError,
-        message:
-          "Unsupported mapping to NIOCore/SocketAddress for GRPCHTTP2Core/SocketAddress: \(socketAddress)."
-      )
-    }
-  }
-}
-
 @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
 extension NIOTSListenerBootstrap {
   fileprivate func bind<Output: Sendable>(

+ 31 - 0
Sources/GRPCHTTP2TransportNIOTransportServices/TLSConfig.swift

@@ -60,4 +60,35 @@ extension HTTP2ServerTransport.TransportServices.Config {
     }
   }
 }
+
+@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
+extension HTTP2ClientTransport.TransportServices.Config {
+  /// The security configuration for this connection.
+  public struct TransportSecurity: Sendable {
+    package enum Wrapped: Sendable {
+      case plaintext
+      case tls(TLS)
+    }
+
+    package let wrapped: Wrapped
+
+    /// This connection is plaintext: no encryption will take place.
+    public static let plaintext = Self(wrapped: .plaintext)
+
+    /// This connection will use TLS.
+    public static func tls(_ tls: TLS) -> Self {
+      Self(wrapped: .tls(tls))
+    }
+  }
+
+  public struct TLS: Sendable {
+    /// A provider for the `SecIdentity` to be used when setting up TLS.
+    public var identityProvider: @Sendable () throws -> SecIdentity
+
+    /// Create a new HTTP2 NIO Transport Services transport TLS config.
+    public init(identityProvider: @Sendable @escaping () throws -> SecIdentity) {
+      self.identityProvider = identityProvider
+    }
+  }
+}
 #endif

+ 22 - 0
Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift

@@ -170,6 +170,28 @@ final class HTTP2TransportNIOPosixTests: XCTestCase {
     }
   }
 
+  func testServerConfig_Defaults() throws {
+    let grpcConfig = HTTP2ServerTransport.Posix.Config.defaults(
+      transportSecurity: .plaintext
+    )
+
+    XCTAssertEqual(grpcConfig.compression, HTTP2ServerTransport.Config.Compression.defaults)
+    XCTAssertEqual(grpcConfig.connection, HTTP2ServerTransport.Config.Connection.defaults)
+    XCTAssertEqual(grpcConfig.http2, HTTP2ServerTransport.Config.HTTP2.defaults)
+    XCTAssertEqual(grpcConfig.rpc, HTTP2ServerTransport.Config.RPC.defaults)
+  }
+
+  func testClientConfig_Defaults() throws {
+    let grpcConfig = HTTP2ClientTransport.Posix.Config.defaults(
+      transportSecurity: .plaintext
+    )
+
+    XCTAssertEqual(grpcConfig.compression, HTTP2ClientTransport.Config.Compression.defaults)
+    XCTAssertEqual(grpcConfig.connection, HTTP2ClientTransport.Config.Connection.defaults)
+    XCTAssertEqual(grpcConfig.http2, HTTP2ClientTransport.Config.HTTP2.defaults)
+    XCTAssertEqual(grpcConfig.backoff, HTTP2ClientTransport.Config.Backoff.defaults)
+  }
+
   #if canImport(NIOSSL)
   static let samplePemCert = """
     -----BEGIN CERTIFICATE-----

+ 25 - 1
Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift

@@ -199,13 +199,37 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase {
     }
   }
 
-  func testTLSConfig_Defaults() throws {
+  func testServerConfig_Defaults() throws {
     let identityProvider = Self.loadIdentity
     let grpcTLSConfig = HTTP2ServerTransport.TransportServices.Config.TLS.defaults(
       identityProvider: identityProvider
     )
+    let grpcConfig = HTTP2ServerTransport.TransportServices.Config.defaults(
+      transportSecurity: .tls(grpcTLSConfig)
+    )
+
+    XCTAssertEqual(grpcConfig.compression, HTTP2ServerTransport.Config.Compression.defaults)
+    XCTAssertEqual(grpcConfig.connection, HTTP2ServerTransport.Config.Connection.defaults)
+    XCTAssertEqual(grpcConfig.http2, HTTP2ServerTransport.Config.HTTP2.defaults)
+    XCTAssertEqual(grpcConfig.rpc, HTTP2ServerTransport.Config.RPC.defaults)
     XCTAssertEqual(try grpcTLSConfig.identityProvider(), try identityProvider())
     XCTAssertEqual(grpcTLSConfig.requireALPN, false)
   }
+
+  func testClientConfig_Defaults() throws {
+    let identityProvider = Self.loadIdentity
+    let grpcTLSConfig = HTTP2ClientTransport.TransportServices.Config.TLS(
+      identityProvider: identityProvider
+    )
+    let grpcConfig = HTTP2ClientTransport.TransportServices.Config.defaults(
+      transportSecurity: .tls(grpcTLSConfig)
+    )
+
+    XCTAssertEqual(grpcConfig.compression, HTTP2ClientTransport.Config.Compression.defaults)
+    XCTAssertEqual(grpcConfig.connection, HTTP2ClientTransport.Config.Connection.defaults)
+    XCTAssertEqual(grpcConfig.http2, HTTP2ClientTransport.Config.HTTP2.defaults)
+    XCTAssertEqual(grpcConfig.backoff, HTTP2ClientTransport.Config.Backoff.defaults)
+    XCTAssertEqual(try grpcTLSConfig.identityProvider(), try identityProvider())
+  }
 }
 #endif

+ 17 - 2
Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift

@@ -101,7 +101,7 @@ final class HTTP2TransportTests: XCTestCase {
   }
 
   func forEachClientAndHTTPStatusCodeServer(
-    _ kind: [Transport.Kind] = [.posix],
+    _ kind: [Transport.Kind] = [.posix, .niots],
     _ execute: (ControlClient, Transport.Kind) async throws -> Void
   ) async throws {
     for clientKind in kind {
@@ -207,7 +207,20 @@ final class HTTP2TransportTests: XCTestCase {
       )
 
     case .niots:
-      fatalError("NIOTS isn't supported yet")
+      #if canImport(Network)
+      var serviceConfig = ServiceConfig()
+      serviceConfig.loadBalancingConfig = [.roundRobin]
+      transport = try HTTP2ClientTransport.TransportServices(
+        target: target,
+        config: .defaults(transportSecurity: .plaintext) {
+          $0.compression.algorithm = compression
+          $0.compression.enabledAlgorithms = enabledCompression
+        },
+        serviceConfig: serviceConfig
+      )
+      #else
+      throw XCTSkip("Transport not supported on this platform")
+      #endif
     }
 
     return GRPCClient(transport: transport)
@@ -1421,7 +1434,9 @@ final class HTTP2TransportTests: XCTestCase {
 extension [HTTP2TransportTests.Transport] {
   static let supported = [
     HTTP2TransportTests.Transport(server: .posix, client: .posix),
+    HTTP2TransportTests.Transport(server: .niots, client: .niots),
     HTTP2TransportTests.Transport(server: .niots, client: .posix),
+    HTTP2TransportTests.Transport(server: .posix, client: .niots),
   ]
 }