Browse Source

TLS Tests (#411)

* Update tests for TLS

* Renaming of sample certs to test data, documentation

* Split up test helpers

* Remove EchoNIO certificate and key symlinks

* Update LinuxMain

* Some renaming and minor nit fixes
George Barnett 6 years ago
parent
commit
ef54360a63

+ 7 - 2
Package.swift

@@ -89,14 +89,19 @@ let package = Package(
     .target(name: "EchoNIO",
             dependencies: [
               "SwiftGRPCNIO",
+              "SwiftGRPCNIOSampleData",
               "SwiftProtobuf",
               "Commander"],
             path: "Sources/Examples/EchoNIO"),
     .target(name: "Simple",
             dependencies: ["SwiftGRPC", "Commander"],
             path: "Sources/Examples/Simple"),
-    .testTarget(name: "SwiftGRPCTests", dependencies: ["SwiftGRPC"]),
-    .testTarget(name: "SwiftGRPCNIOTests", dependencies: ["SwiftGRPC", "SwiftGRPCNIO"])
+    .target(name: "SwiftGRPCNIOSampleData",
+            dependencies: ["NIOSSL"]),
+    .testTarget(name: "SwiftGRPCTests",
+                dependencies: ["SwiftGRPC"]),
+    .testTarget(name: "SwiftGRPCNIOTests",
+                dependencies: ["SwiftGRPC", "SwiftGRPCNIO", "SwiftGRPCNIOSampleData"]),
   ],
   cLanguageStandard: .gnu11,
   cxxLanguageStandard: .cxx11)

+ 24 - 15
Sources/Examples/EchoNIO/main.swift

@@ -19,6 +19,7 @@ import Foundation
 import NIO
 import NIOSSL
 import SwiftGRPCNIO
+import SwiftGRPCNIOSampleData
 
 // Common flags and options
 let sslFlag = Flag("ssl", description: "if true, use SSL for connections")
@@ -31,21 +32,6 @@ let messageOption = Option("message",
                            default: "Testing 1 2 3",
                            description: "message to send")
 
-func makeClientTLSConfiguration() throws -> TLSConfiguration {
-  let certificate = try NIOSSLCertificate(file: "ssl.crt", format: .pem)
-  // The certificate common name is "example.com", so skip hostname verification.
-  return .forClient(certificateVerification: .noHostnameVerification,
-                    trustRoots: .certificates([certificate]))
-}
-
-func makeServerTLSConfiguration() throws -> TLSConfiguration {
-  let certificate = try NIOSSLCertificate(file: "ssl.crt", format: .pem)
-  let key = try NIOSSLPrivateKey(file: "ssl.key", format: .pem)
-  return .forServer(certificateChain: [.certificate(certificate)],
-                    privateKey: .privateKey(key),
-                    trustRoots: .certificates([certificate]))
-}
-
 func makeClientTLS(enabled: Bool) throws -> GRPCClient.TLSMode {
   guard enabled else {
     return .none
@@ -60,6 +46,29 @@ func makeServerTLS(enabled: Bool) throws -> GRPCServer.TLSMode {
   return .custom(try NIOSSLContext(configuration: try makeServerTLSConfiguration()))
 }
 
+func makeClientTLSConfiguration() throws -> TLSConfiguration {
+  let caCert = SampleCertificate.ca
+  let clientCert = SampleCertificate.client
+  precondition(!caCert.isExpired && !clientCert.isExpired,
+               "SSL certificates are expired. Please submit an issue at https://github.com/grpc/grpc-swift.")
+
+  return .forClient(certificateVerification: .noHostnameVerification,
+                    trustRoots: .certificates([caCert.certificate]),
+                    certificateChain: [.certificate(clientCert.certificate)],
+                    privateKey: .privateKey(SamplePrivateKey.client))
+}
+
+func makeServerTLSConfiguration() throws -> TLSConfiguration {
+  let caCert = SampleCertificate.ca
+  let serverCert = SampleCertificate.server
+  precondition(!caCert.isExpired && !serverCert.isExpired,
+               "SSL certificates are expired. Please submit an issue at https://github.com/grpc/grpc-swift.")
+
+  return .forServer(certificateChain: [.certificate(serverCert.certificate)],
+                    privateKey: .privateKey(SamplePrivateKey.server),
+                    trustRoots: .certificates([caCert.certificate]))
+}
+
 /// Create en `EchoClient` and wait for it to initialize. Returns nil if initialisation fails.
 func makeEchoClient(address: String, port: Int, ssl: Bool) -> Echo_EchoService_NIOClient? {
   let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)

+ 0 - 1
Sources/Examples/EchoNIO/ssl.crt

@@ -1 +0,0 @@
-../Echo/ssl.crt

+ 0 - 1
Sources/Examples/EchoNIO/ssl.key

@@ -1 +0,0 @@
-../Echo/ssl.key

+ 178 - 0
Sources/SwiftGRPCNIOSampleData/GRPCSwiftCertificate.swift

@@ -0,0 +1,178 @@
+/*
+ * Copyright 2019, 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.
+ */
+import Foundation
+import NIOSSL
+
+/// Wraps `NIOSSLCertificate` to provide the certificate common name and expiry date.
+public struct SampleCertificate {
+  public var certificate: NIOSSLCertificate
+  public var commonName: String
+  public var notAfter: Date
+
+  public static let ca = SampleCertificate(
+    certificate: try! NIOSSLCertificate(buffer: Array(caCert.utf8CString), format: .pem),
+    commonName: "foo",
+    notAfter: Date(timeIntervalSince1970: 1584530912.0))
+
+  public static let server = SampleCertificate(
+    certificate: try! NIOSSLCertificate(buffer: Array(serverCert.utf8CString), format: .pem),
+    commonName: "example.com",
+    // 18/03/2020 11:28:33
+    notAfter: Date(timeIntervalSince1970: 1584530913.0))
+
+  public static let client = SampleCertificate(
+      certificate: try! NIOSSLCertificate(buffer: Array(clientCert.utf8CString), format: .pem),
+      commonName: "localhost",
+      // 18/03/2020 11:28:35
+      notAfter: Date(timeIntervalSince1970: 1584530915.0))
+}
+
+extension SampleCertificate {
+  /// Returns whether the certificate has expired.
+  public var isExpired: Bool {
+    return notAfter < Date()
+  }
+}
+
+/// Provides convenience methods to make `NIOSSLPrivateKey`s for corresponding `GRPCSwiftCertificate`s.
+public struct SamplePrivateKey {
+  private init() { }
+
+  public static let server = try! NIOSSLPrivateKey(buffer: Array(serverKey.utf8CString), format: .pem)
+  public static let client = try! NIOSSLPrivateKey(buffer: Array(clientKey.utf8CString), format: .pem)
+}
+
+// MARK: - Certificates and private keys
+
+// NOTE: use the "makecerts" script in the scripts directory to generate new
+// certificates and private keys when these expire.
+
+private let caCert = """
+    -----BEGIN CERTIFICATE-----
+    MIICmDCCAYACCQDGbQdNHHqGqDANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANm
+    b28wHhcNMTkwMzE5MTEyODMyWhcNMjAwMzE4MTEyODMyWjAOMQwwCgYDVQQDDANm
+    b28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZejSltOdp41GdU58N
+    pzwpz6NBKGBQ3Hvh+Gj5p0th6PbZxKXNynaca0eSXsDOifRX0AWpLPhmxgMlQ0Yj
+    4npYVbef3E+yCOX1agGP228YrTwGChPvsCSiYLrx9iBLlxYosIyM2A2RnhrTxR8W
+    0Zf3ANJVvKBKrLIFzStqf6317oBLdAH3txxWYVycdQWTlp3Fe+2seOyQbmi9CqPp
+    dmDqMrbNBqpDm54VsGDBAyUo7Jwntyno7qbSpmFVHlTORdvmu94UccJrspH3AHzB
+    yfQ6EC5xXpbXrJtFzwQJ+Uh3MXPeIvvEP9qOL3iuuHJajOpaFRD822Br7L913/Jq
+    OqEhAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEAK72aJaU4mBjHV5zY6QpYTQ1Gc
+    L5BC1WZTKKb1swp4sLL7KQEFewr7U/52T6i9rmep2qXVbQftFNgA2e1Gis7ws9Gj
+    FfnvZVzXl3OBcba4siJSpjyyCZ+g6cd/FHdRWI4wyn0XhwN6VMCXEvOzmMVRgGWW
+    RL2suwQhhsgMjKpdYs3XihUxaFxU/Uhd5bCPhFMg5WvUVZ8koMmkN/VT5geJPnZW
+    xcZdNHCKpWQwPnfUEGgfFHVkvaJvf9gkkzZizEXXt7WyHiZ7lak5iI1O2pEjlpLW
+    4+t1wS6/qBAYL+bmT6rn74cvF1P+tlTjRiFn3VR0ofdQbhTllgPwWkdGeko=
+    -----END CERTIFICATE-----
+    """
+
+private let serverCert = """
+    -----BEGIN CERTIFICATE-----
+    MIICmDCCAYACAQEwDQYJKoZIhvcNAQEFBQAwDjEMMAoGA1UEAwwDZm9vMB4XDTE5
+    MDMxOTExMjgzM1oXDTIwMDMxODExMjgzM1owFjEUMBIGA1UEAwwLZXhhbXBsZS5j
+    b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDS5ph7oNIX0Zc2IgeY
+    6IXfV8Ecqct1ACcRBDzjofehKJoWK9z0DnK+H14yUKsai6n4y9l7k41TM13vv0q8
+    ExkmDdO902uqi9h1f8ifE4K5UWMTUqoSh+ZhhEH5W8cr5wdpnfImziTCXbSKcZ6/
+    /4MGY4YO9/MxCNebcvAoPjqfaem+xU0cuoJaKVF61HNh2tQ5QtMQRarnPJZiXDpm
+    aIwXVEra8/2EDVqljoQbK9cHe9koB6FjlmWvidnjF8zYOiP9unVu0665/IaQSWOH
+    wEDe44HuC/eLWeJ16Y368CNAHaLpMhIwKWNHSmWlGyVgYFaWCmuHsSGNLkWgaSNn
+    O3z/AgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAGpAZgAfjjwnyufM05ruCS532q//
+    +Pv1FjcwebD1rRssM0uFZLYqcL59BdZ2CyQ7RgyGElTH9kZMW5ishWWTjnkqUG6S
+    WL9sR23UGO1kIIHt/Q2PElo5e94/zrOHj/j44YeU+6nKlqES+eecvzZ8em1Del9I
+    kXeQEq8/bcjt2vk1VzCGNxaYYerAafBaZQ6xRjl8eAPBrPgFdbXpZ1ohcEXTfds+
+    wG2zhQbH37DdSZ4M/Kx/1iMgOZ25cOlMTk355HXaBEJDq4LOlUwMn0AZvskxHU7E
+    yKY0Idsg519jJvW3ZgCZ6FMkher809H1TgpdK3zF3o96PxN2NjZhwSKXmO8=
+    -----END CERTIFICATE-----
+    """
+
+private let serverKey = """
+    -----BEGIN RSA PRIVATE KEY-----
+    MIIEpQIBAAKCAQEA0uaYe6DSF9GXNiIHmOiF31fBHKnLdQAnEQQ846H3oSiaFivc
+    9A5yvh9eMlCrGoup+MvZe5ONUzNd779KvBMZJg3TvdNrqovYdX/InxOCuVFjE1Kq
+    EofmYYRB+VvHK+cHaZ3yJs4kwl20inGev/+DBmOGDvfzMQjXm3LwKD46n2npvsVN
+    HLqCWilRetRzYdrUOULTEEWq5zyWYlw6ZmiMF1RK2vP9hA1apY6EGyvXB3vZKAeh
+    Y5Zlr4nZ4xfM2Doj/bp1btOuufyGkEljh8BA3uOB7gv3i1nidemN+vAjQB2i6TIS
+    MCljR0plpRslYGBWlgprh7EhjS5FoGkjZzt8/wIDAQABAoIBAHmKzWvKFeoGLvfS
+    isBTmPtK7o7fR9LI4LrMz258ZGKrLIoEg1Tfkr8BAt9KYCFvReiNSmwOcA739nX5
+    r09OTlsA8vteAZmK+JdWqj8LFnZIcimrpToCugGPIBpeCx3BCiOTE//LI6IkMKzs
+    qAmMbm1bI+IygSPMLb13cvIuUsiVTH8oALd3bNM/unMIsTOXPaRwUEvskLMDoGZT
+    z9J0ox7V+ziVXpB8qXMUEn1sB1USpdNcu47seKI8utOFCQ0v0KEW9xaV4wo3nT8W
+    uXJifI3pSesq3MddSA7iE+2wO/ngBN+14rbmg9Rivu2Zk5jVP5T0h1ENvhkXmcli
+    lvjycykCgYEA6HR1CP7t8h6pwPArgIRyPT7oCkUQwN0oae59ZVJH74uZhanTm2p4
+    2Qz+Xp8Ee4I3A2JDyYNouzwVC9JOTSXjfvrxMaajGoMdacmNJ2UZ4/6Xww2gvikp
+    MHPUwg6nKSQcu8Bo+/nhxHHBxfdSIEgvzUzXvirUQHLpbLX9Z8WM4zUCgYEA6EM9
+    m2MoaSVIa6TBJQwwCDwHBCr3xaMoo/obsruCE231r0ZPh8DAlZRSwPoNa2oWzJvI
+    6DFaIEMem60HXWzifY54jPQ70crMRehSGcNdUq+hbZPh9J5mMQxEiR7Ck9B7ijNE
+    F4PONQeNQEMPvZ4CdNCaK1lfKLoy7wvOkyLJkeMCgYEAoB2Hd/jRQZMpboKAFHgm
+    kFVCU8Ca953edokVyrLQZgoMZ2tBHK5MK4WtuNNjrQdWiXgoJSfk/gM2o/vqf213
+    tEF53a9gbaSen/16wwX6vXbiZjJ+5D1J59wBUuHw9n+vYwv3xIisoDmTNZ9T7HSM
+    qKcjfBPYO8RrULxSniYPE3kCgYEA4se0waIB9RhYK/KEPB44T/H8j888ehcjOWid
+    3thC26HD/83RHaXQ5LwcSRxeOgEuHb4GXuDBNTsUCcDarhgA1cNkZYybU+6FocSD
+    VXByEKg4IHwCZgy7jyyBRrloF1e7KGeCFsu1bgXfn11bYzODBngf8C+lQGj+DnYi
+    z3tqAS0CgYEA0D6XZjezamgu7A9/zfidkvq154vIuYP7x3KmY1nBmkBZ9RsvcXZo
+    wXwr1e0BNYx8ARwgsK/IJveIRcfrLIAM0gKbEi/WDRNUplobaPQJU5D9t3ptaO3s
+    qHBR2ObzN2h8n6/IqXcUd57QWERQJAXmKn90erryBgnfP0f2bDBDOB8=
+    -----END RSA PRIVATE KEY-----
+    """
+
+private let clientCert = """
+    -----BEGIN CERTIFICATE-----
+    MIICljCCAX4CAQEwDQYJKoZIhvcNAQEFBQAwDjEMMAoGA1UEAwwDZm9vMB4XDTE5
+    MDMxOTExMjgzNVoXDTIwMDMxODExMjgzNVowFDESMBAGA1UEAwwJbG9jYWxob3N0
+    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoGJAQPCdIQlzMJ7aUMW
+    dMPK8/MGGmtDmh4CNmS7eGuF4STxn10ca/yd+GqlXJbV38u3+4DiCKnv8oX7keKZ
+    eovVJsNLs+7Lc+YPNlIrzYSAed6bzTaIyrOQVed7UCMt9cVxgw7rVcRgCQYNbuxe
+    ZDTEfFNqqZ7G2g6X1AszCc+pmrwWPBBAOeJIPXkVrMEVWD9BczvptxA21N5bzGqs
+    oM1v7qdTNMMsXXz2fFoUdiYgaw1aGCuPjyfPBHGFJ6arQkvwy/AxYj7TToHFNUgo
+    RXD7uRonl2DFHyfLI5E1Fmi9GtYUAvc9Zsle94yjEpa5wQtunboqviMvNz6cbTCH
+    dQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQClCN5SJRmaQsPKlnVLlFAGuM400lsc
+    DjkxZ+w5H3EmntnWVXOCAwh0/GWbk6FLLa8NdXikWX8/TOnUCGrETSEHp/oCA3Jw
+    7rb0QDwLFdGCpAaxs5lRzppTro6rVOANW22h+whJ1E0YmeBYdy9ptAq4m4DCu7LX
+    POATR+KgO2rXCWC4RPt0ZkP0r4S/gMwuW/ciDAcnC5GdyOvLjmAlxmoDZqa6CUmC
+    V6r7twDyBlQ7fdsNmXe9YEdO34T6OWxl3gLSwmAG/EYiejTwpQvbJKZFAKghmF3s
+    dfpLlHaroeBFmniRiZokRkGT6gkUUNBXCFbZB0Nh1D0onGJS5bhGFVm0
+    -----END CERTIFICATE-----
+    """
+
+private let clientKey = """
+    -----BEGIN RSA PRIVATE KEY-----
+    MIIEogIBAAKCAQEAxoGJAQPCdIQlzMJ7aUMWdMPK8/MGGmtDmh4CNmS7eGuF4STx
+    n10ca/yd+GqlXJbV38u3+4DiCKnv8oX7keKZeovVJsNLs+7Lc+YPNlIrzYSAed6b
+    zTaIyrOQVed7UCMt9cVxgw7rVcRgCQYNbuxeZDTEfFNqqZ7G2g6X1AszCc+pmrwW
+    PBBAOeJIPXkVrMEVWD9BczvptxA21N5bzGqsoM1v7qdTNMMsXXz2fFoUdiYgaw1a
+    GCuPjyfPBHGFJ6arQkvwy/AxYj7TToHFNUgoRXD7uRonl2DFHyfLI5E1Fmi9GtYU
+    Avc9Zsle94yjEpa5wQtunboqviMvNz6cbTCHdQIDAQABAoIBAA61Wmlw1d+8SvC5
+    GFvcVLWiLE+XGkSq3f91acSOAjYSAYGFM0ITrB90QGA/xrDtnDtQ5PkFu7nYnabi
+    tplAqQ6jfc+5eMqETx7vVQE5ZXV88+gTzoeOGuSqGW/EDycI9EbZsmd7m4RnYJZK
+    lIQ7j2LtZgGwTJ703NcbbbSQf9+iGeXl0Bc7RPFzXdFVS30Kj3Gj2t3YIeuHfUz1
+    Xo2YozIbhdosW38ryAbxYeBfYaF6wH00XYclyFYEe1krX4jfwwTUHF2xP2vymV8H
+    GoKIuERWM6jYW3TXmyrCHrWPMyA3uwTChat47DUuVanAdJaA+B7MowZ9IQJy0hJn
+    J95jQqECgYEA+J4d+NGZQpN7qTUKknmhROCJkyiQyn2XbE0ymBRXZwVqPCB43Axo
+    UD7IorF73geFKJdcFjnZlmnNkFFrablTqk6rMa88vUeeawyWggwXfFzDxECFUR7n
+    e9iPlv3ygtaQaApWzRLvIz76XN0UgogCEu2sYZ1B5ETunt+qr3kZ5vsCgYEAzGZ9
+    vxm77+fBVdK0sKAJpaZZgKdLZeKEjmhSK0/yVX0W3k/2fvwcD9nirRwy4MWy/9en
+    a7/HquwprA4wPqI8cijQV0R4j6pV5kYcho3RNY2o/9nLBCq2BGgONx23iw7FmtZt
+    A5Bek7fpbaTmtlDuImjXJvw+hcH3LBzj8AkCwE8CgYAOLDVZMdmiyfWKt9NadkST
+    QJmXIgDfCjnPmrb/pGk3Hj/oHZHGOY7YxDt7ytJc3eDhZ3+AZNvajz2AtKOC62Wx
+    l7p6opq7z5FgWN9bmoTcOg2O6n6vGSvpC3dkDCX+/2xMAgrgteub/sMW+CNrLYWw
+    vovNJMHU2Xkg5W89gZHQcQKBgC30BOVH1dbT1cWDv5fOAx04zvp7ohnf2Uli7sZK
+    DQNnQhLtC0/1QiHWLH4azt111Q5r33n7/dnRinTiI7qRIuHPhzd3b1ttQi6pKJSf
+    oZ9Wn94Viuz+5TkMY9XEWpVq1sY+2vdoJ7syJ8q8vhnTDBa0V1qubygHOZizThOT
+    EwlFAoGALyfmW00GB4LPX73QJ+i+TzBloRpS+Epk3UAaUChzCytlCzZpEO2YiDrz
+    /rJWYC4iq81UtI/iqrSMNg1K3LRCJdnfv47q2g7d4OZntUyI2dUVzGSfO6K+u5Ip
+    mHNAjWkgR2RjHoHNxKHFN8evEc2aypUSKnHqxrnzwCCVvLNLk90=
+    -----END RSA PRIVATE KEY-----
+    """

+ 3 - 1
Tests/LinuxMain.swift

@@ -38,7 +38,9 @@ XCTMain([
   testCase(ServerTimeoutTests.allTests),
 
   // SwiftGRPCNIO
-  testCase(NIOServerTests.allTests),
+  testCase(NIOFunctionalTestsInsecureTransport.allTests),
+  testCase(NIOFunctionalTestsAnonymousClient.allTests),
+  testCase(NIOFunctionalTestsMutualAuthentication.allTests),
   testCase(SwiftGRPCNIOTests.ServerThrowingTests.allTests),
   testCase(SwiftGRPCNIOTests.ServerDelayedThrowingTests.allTests),
   testCase(SwiftGRPCNIOTests.ClientThrowingWhenServerReturningErrorTests.allTests),

+ 42 - 0
Tests/SwiftGRPCNIOTests/EventLoopFuture+Assertions.swift

@@ -0,0 +1,42 @@
+/*
+ * Copyright 2019, 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.
+ */
+import Foundation
+import NIO
+import XCTest
+
+extension EventLoopFuture where Value: Equatable {
+  /// Registers a callback which asserts the value promised by this future is equal to
+  /// the expected value. Causes a test failure if the future returns an error.
+  ///
+  /// - Parameters:
+  ///   - expected: The expected value.
+  ///   - expectation: A test expectation to fulfill once the future has completed.
+  func assertEqual(_ expected: Value, fulfill expectation: XCTestExpectation, file: StaticString = #file, line: UInt = #line) {
+    self.whenComplete { result in
+      defer {
+        expectation.fulfill()
+      }
+
+      switch result {
+      case .success(let actual):
+        XCTAssertEqual(expected, actual, file: file, line: line)
+
+      case .failure(let error):
+        XCTFail("Expecteded '\(expected)' but received error: \(error)", file: file, line: line)
+      }
+    }
+  }
+}

+ 7 - 7
Tests/SwiftGRPCNIOTests/GRPCChannelHandlerTests.swift

@@ -22,7 +22,7 @@ class GRPCChannelHandlerTests: GRPCChannelHandlerResponseCapturingTestCase {
     let expectedError = GRPCServerError.unimplementedMethod("unimplementedMethodName")
     XCTAssertEqual([expectedError], errorCollector.asGRPCServerErrors)
 
-    XCTAssertNoThrow(try extractStatus(responses[0])) { status in
+    responses[0].assertStatus { status in
       XCTAssertEqual(status, expectedError.asGRPCStatus())
     }
   }
@@ -39,10 +39,10 @@ class GRPCChannelHandlerTests: GRPCChannelHandlerResponseCapturingTestCase {
       try channel.writeInbound(RawGRPCServerRequestPart.message(buffer))
     }
 
-    XCTAssertNoThrow(try extractHeaders(responses[0]))
-    XCTAssertNoThrow(try extractMessage(responses[1]))
-    XCTAssertNoThrow(try extractStatus(responses[2])) { status in
-      XCTAssertEqual(status, .ok)
+    responses[0].assertHeaders()
+    responses[1].assertMessage()
+    responses[2].assertStatus { status in
+      XCTAssertEqual(status.code, .ok)
     }
   }
 
@@ -59,8 +59,8 @@ class GRPCChannelHandlerTests: GRPCChannelHandlerResponseCapturingTestCase {
     let expectedError = GRPCServerError.requestProtoDeserializationFailure
     XCTAssertEqual([expectedError], errorCollector.asGRPCServerErrors)
 
-    XCTAssertNoThrow(try extractHeaders(responses[0]))
-    XCTAssertNoThrow(try extractStatus(responses[1])) { status in
+    responses[0].assertHeaders()
+    responses[1].assertStatus { status in
       XCTAssertEqual(status, expectedError.asGRPCStatus())
     }
   }

+ 12 - 12
Tests/SwiftGRPCNIOTests/HTTP1ToRawGRPCServerCodecTests.swift

@@ -36,8 +36,8 @@ class HTTP1ToRawGRPCServerCodecTests: GRPCChannelHandlerResponseCapturingTestCas
     let expectedError = GRPCCommonError.unexpectedCompression
     XCTAssertEqual([expectedError], errorCollector.asGRPCCommonErrors)
 
-    XCTAssertNoThrow(try extractHeaders(responses[0]))
-    XCTAssertNoThrow(try extractStatus(responses[1])) { status in
+    responses[0].assertHeaders()
+    responses[1].assertStatus { status in
       XCTAssertEqual(status, expectedError.asGRPCStatus())
     }
   }
@@ -64,9 +64,9 @@ class HTTP1ToRawGRPCServerCodecTests: GRPCChannelHandlerResponseCapturingTestCas
       try channel.writeInbound(HTTPServerRequestPart.body(buffer))
     }
 
-    XCTAssertNoThrow(try extractHeaders(responses[0]))
-    XCTAssertNoThrow(try extractMessage(responses[1]))
-    XCTAssertNoThrow(try extractStatus(responses[2])) { status in
+    responses[0].assertHeaders()
+    responses[1].assertMessage()
+    responses[2].assertStatus { status in
       XCTAssertEqual(status, .ok)
     }
   }
@@ -83,8 +83,8 @@ class HTTP1ToRawGRPCServerCodecTests: GRPCChannelHandlerResponseCapturingTestCas
     let expectedError = GRPCServerError.requestProtoDeserializationFailure
     XCTAssertEqual([expectedError], errorCollector.asGRPCServerErrors)
 
-    XCTAssertNoThrow(try extractHeaders(responses[0]))
-    XCTAssertNoThrow(try extractStatus(responses[1])) { status in
+    responses[0].assertHeaders()
+    responses[1].assertStatus { status in
       XCTAssertEqual(status, expectedError.asGRPCStatus())
     }
   }
@@ -111,8 +111,8 @@ class HTTP1ToRawGRPCServerCodecTests: GRPCChannelHandlerResponseCapturingTestCas
       XCTFail("\(String(describing: errorCollector.errors.first)) was not .invalidState")
     }
 
-    XCTAssertNoThrow(try extractHeaders(responses[0]))
-    XCTAssertNoThrow(try extractStatus(responses[1])) { status in
+    responses[0].assertHeaders()
+    responses[1].assertStatus { status in
       XCTAssertEqual(status, .processingError)
     }
   }
@@ -133,9 +133,9 @@ class HTTP1ToRawGRPCServerCodecTests: GRPCChannelHandlerResponseCapturingTestCas
       try channel.writeInbound(HTTPServerRequestPart.end(trailers))
     }
 
-    XCTAssertNoThrow(try extractHeaders(responses[0]))
-    XCTAssertNoThrow(try extractMessage(responses[1]))
-    XCTAssertNoThrow(try extractStatus(responses[2])) { status in
+    responses[0].assertHeaders()
+    responses[1].assertMessage()
+    responses[2].assertStatus { status in
       XCTAssertEqual(status, .ok)
     }
   }

+ 104 - 19
Tests/SwiftGRPCNIOTests/NIOBasicEchoTestCase.swift

@@ -16,7 +16,9 @@
 import Dispatch
 import Foundation
 import NIO
+import NIOSSL
 @testable import SwiftGRPCNIO
+import SwiftGRPCNIOSampleData
 import XCTest
 
 extension Echo_EchoRequest {
@@ -31,41 +33,124 @@ extension Echo_EchoResponse {
   }
 }
 
-class NIOBasicEchoTestCase: XCTestCase {
+enum TransportSecurity {
+  case none
+  case anonymousClient
+  case mutualAuthentication
+}
+
+extension TransportSecurity {
+  var caCert: NIOSSLCertificate {
+    let cert = SampleCertificate.ca
+    cert.assertNotExpired()
+    return cert.certificate
+  }
+
+  var clientCert: NIOSSLCertificate {
+    let cert = SampleCertificate.client
+    cert.assertNotExpired()
+    return cert.certificate
+  }
+
+  var serverCert: NIOSSLCertificate {
+    let cert = SampleCertificate.server
+    cert.assertNotExpired()
+    return cert.certificate
+  }
+}
+
+extension TransportSecurity {
+  func makeServerTLS() throws -> GRPCServer.TLSMode {
+    return try makeServerTLSConfiguration().map { .custom(try NIOSSLContext(configuration: $0)) } ?? .none
+  }
+
+  func makeServerTLSConfiguration() throws -> TLSConfiguration? {
+    switch self {
+    case .none:
+      return nil
+
+    case .anonymousClient, .mutualAuthentication:
+      return .forServer(certificateChain: [.certificate(self.serverCert)],
+                        privateKey: .privateKey(SamplePrivateKey.server), 
+                        trustRoots: .certificates ([self.caCert]))
+    }
+  }
+
+  func makeClientTLS() throws -> GRPCClient.TLSMode {
+    return try makeClientTLSConfiguration().map { .custom(try NIOSSLContext(configuration: $0)) } ?? .none
+  }
+
+  func makeClientTLSConfiguration() throws -> TLSConfiguration? {
+    switch self {
+    case .none:
+      return nil
+
+    case .anonymousClient:
+      return .forClient(certificateVerification: .noHostnameVerification,
+                        trustRoots: .certificates([self.caCert]))
+
+    case .mutualAuthentication:
+      return .forClient(certificateVerification: .noHostnameVerification,
+                        trustRoots: .certificates([self.caCert]),
+                        certificateChain: [.certificate(self.clientCert)],
+                        privateKey: .privateKey(SamplePrivateKey.client))
+    }
+  }
+}
+
+class NIOEchoTestCaseBase: XCTestCase {
   var defaultTestTimeout: TimeInterval = 1.0
 
-  var serverEventLoopGroup: EventLoopGroup!
-  var server: GRPCServer!
+  let serverEventLoopGroup: EventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+  let clientEventLoopGroup: EventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
 
-  var clientEventLoopGroup: EventLoopGroup!
+  var transportSecurity: TransportSecurity {
+    return .none
+  }
+
+  var server: GRPCServer!
   var client: Echo_EchoService_NIOClient!
-  
-  func makeEchoProvider() -> Echo_EchoProvider_NIO { return EchoProviderNIO() }
 
-  override func setUp() {
-    super.setUp()
+  func makeServer() throws -> GRPCServer {
+    return try GRPCServer.start(
+      hostname: "localhost",
+      port: 5050,
+      eventLoopGroup: self.serverEventLoopGroup,
+      serviceProviders: [makeEchoProvider()],
+      tls: try self.transportSecurity.makeServerTLS()
+    ).wait()
+  }
+
+  func makeClient() throws -> GRPCClient {
+    return try GRPCClient.start(
+      host: "localhost",
+      port: 5050,
+      eventLoopGroup: self.clientEventLoopGroup,
+      tls: try self.transportSecurity.makeClientTLS()
+    ).wait()
+  }
+
+  func makeEchoProvider() -> Echo_EchoProvider_NIO {
+    return EchoProviderNIO()
+  }
 
-    self.serverEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
-    self.server = try! GRPCServer.start(
-      hostname: "localhost", port: 5050, eventLoopGroup: self.serverEventLoopGroup, serviceProviders: [makeEchoProvider()])
-      .wait()
+  func makeEchoClient() throws -> Echo_EchoService_NIOClient {
+    return Echo_EchoService_NIOClient(client: try self.makeClient())
+  }
 
-    self.clientEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
-    self.client = try! GRPCClient.start(
-      host: "localhost", port: 5050, eventLoopGroup: self.clientEventLoopGroup)
-      .map { Echo_EchoService_NIOClient(client: $0, defaultCallOptions: CallOptions(timeout: try! .seconds(5))) }
-      .wait()
+  override func setUp() {
+    super.setUp()
+    self.server = try! self.makeServer()
+    self.client = try! self.makeEchoClient()
   }
 
   override func tearDown() {
     XCTAssertNoThrow(try self.client.client.close().wait())
     XCTAssertNoThrow(try self.clientEventLoopGroup.syncShutdownGracefully())
-    self.clientEventLoopGroup = nil
     self.client = nil
 
     XCTAssertNoThrow(try self.server.close().wait())
     XCTAssertNoThrow(try self.serverEventLoopGroup.syncShutdownGracefully())
-    self.serverEventLoopGroup = nil
     self.server = nil
 
     super.tearDown()

+ 1 - 1
Tests/SwiftGRPCNIOTests/NIOClientCancellingTests.swift

@@ -17,7 +17,7 @@ import Foundation
 import SwiftGRPCNIO
 import XCTest
 
-class NIOClientCancellingTests: NIOBasicEchoTestCase {
+class NIOClientCancellingTests: NIOEchoTestCaseBase {
   static var allTests: [(String, (NIOClientCancellingTests) -> () throws -> Void)] {
     return [
       ("testUnary", testUnary),

+ 1 - 1
Tests/SwiftGRPCNIOTests/NIOClientTimeoutTests.swift

@@ -18,7 +18,7 @@ import SwiftGRPCNIO
 import NIO
 import XCTest
 
-class NIOClientTimeoutTests: NIOBasicEchoTestCase {
+class NIOClientTimeoutTests: NIOEchoTestCaseBase {
   let optionsWithShortTimeout = CallOptions(timeout: try! GRPCTimeout.milliseconds(10))
   let moreThanShortTimeout: TimeInterval = 0.011
 

+ 266 - 0
Tests/SwiftGRPCNIOTests/NIOFunctionalTests.swift

@@ -0,0 +1,266 @@
+/*
+ * Copyright 2019, 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.
+ */
+import Dispatch
+import Foundation
+import NIO
+import NIOHTTP1
+import NIOHTTP2
+@testable import SwiftGRPCNIO
+import XCTest
+
+protocol NIOFunctionalTests: class {
+  func testUnary() throws
+  func testUnaryLotsOfRequests() throws
+  func testUnaryWithLargeData() throws
+  func testUnaryEmptyRequest() throws
+  func testClientStreaming() throws
+  func testClientStreamingLotsOfMessages() throws
+  func testServerStreaming() throws
+  func testServerStreamingLotsOfMessages() throws
+  func testBidirectionalStreamingBatched() throws
+  func testBidirectionalStreamingPingPong() throws
+  func testBidirectionalStreamingLotsOfMessagesBatched() throws
+  func testBidirectionalStreamingLotsOfMessagesPingPong() throws
+}
+
+extension NIOFunctionalTests {
+  static var allTests: [(String, (Self) -> () throws -> Void)] {
+    return [
+      ("testUnary", testUnary),
+      ("testUnaryLotsOfRequests", testUnaryLotsOfRequests),
+      ("testUnaryWithLargeData", testUnaryWithLargeData),
+      ("testUnaryEmptyRequest", testUnaryEmptyRequest),
+      ("testClientStreaming", testClientStreaming),
+      ("testClientStreamingLotsOfMessages", testClientStreamingLotsOfMessages),
+      ("testServerStreaming", testServerStreaming),
+      ("testServerStreamingLotsOfMessages", testServerStreamingLotsOfMessages),
+      ("testBidirectionalStreamingBatched", testBidirectionalStreamingBatched),
+      ("testBidirectionalStreamingPingPong", testBidirectionalStreamingPingPong),
+      ("testBidirectionalStreamingLotsOfMessagesBatched", testBidirectionalStreamingLotsOfMessagesBatched),
+      ("testBidirectionalStreamingLotsOfMessagesPingPong", testBidirectionalStreamingLotsOfMessagesPingPong)
+    ]
+  }
+}
+
+class NIOFunctionalTestsInsecureTransport: NIOEchoTestCaseBase, NIOFunctionalTests {
+  override var transportSecurity: TransportSecurity {
+    return .none
+  }
+
+  var aFewStrings: [String] {
+    return ["foo", "bar", "baz"]
+  }
+
+  var lotsOfStrings: [String] {
+    return (0..<5_000).map {
+      String(describing: $0)
+    }
+  }
+}
+
+extension NIOFunctionalTestsInsecureTransport {
+  func makeExpectation(description: String, expectedFulfillmentCount: Int = 1, assertForOverFulfill: Bool = true) -> XCTestExpectation {
+    let expectation = self.expectation(description: description)
+    expectation.expectedFulfillmentCount = expectedFulfillmentCount
+    expectation.assertForOverFulfill = assertForOverFulfill
+    return expectation
+  }
+
+  func makeStatusExpectation(expectedFulfillmentCount: Int = 1) -> XCTestExpectation {
+    return makeExpectation(description: "Expecting status received",
+                           expectedFulfillmentCount: expectedFulfillmentCount)
+  }
+
+  func makeResponseExpectation(expectedFulfillmentCount: Int = 1) -> XCTestExpectation {
+    return makeExpectation(description: "Expecting \(expectedFulfillmentCount) response(s)",
+                           expectedFulfillmentCount: expectedFulfillmentCount)
+  }
+}
+
+extension NIOFunctionalTestsInsecureTransport {
+  func doTestUnary(request: Echo_EchoRequest, expect response: Echo_EchoResponse, file: StaticString = #file, line: UInt = #line) {
+    let responseExpectation = self.makeResponseExpectation()
+    let statusExpectation = self.makeStatusExpectation()
+
+    let call = client.get(request)
+    call.response.assertEqual(response, fulfill: responseExpectation, file: file, line: line)
+    call.status.map { $0.code }.assertEqual(.ok, fulfill: statusExpectation, file: file, line: line)
+
+    self.wait(for: [responseExpectation, statusExpectation], timeout: self.defaultTestTimeout)
+  }
+
+  func doTestUnary(message: String, file: StaticString = #file, line: UInt = #line) {
+    self.doTestUnary(request: Echo_EchoRequest(text: message), expect: Echo_EchoResponse(text: "Swift echo get: \(message)"), file: file, line: line)
+  }
+
+  func testUnary() throws {
+    self.doTestUnary(message: "foo")
+  }
+
+  func testUnaryLotsOfRequests() throws {
+    self.defaultTestTimeout = 60.0
+
+    // Sending that many requests at once can sometimes trip things up, it seems.
+    let clockStart = clock()
+    let numberOfRequests = 2_000
+    let responseExpectation = self.makeResponseExpectation(expectedFulfillmentCount: numberOfRequests)
+    let statusExpectation = self.makeStatusExpectation(expectedFulfillmentCount: numberOfRequests)
+
+    for i in 0..<numberOfRequests {
+      if i % 1_000 == 0 && i > 0 {
+        print("\(i) requests sent so far, elapsed time: \(Double(clock() - clockStart) / Double(CLOCKS_PER_SEC))")
+      }
+
+      let request = Echo_EchoRequest(text: "foo \(i)")
+      let response = Echo_EchoResponse(text: "Swift echo get: foo \(i)")
+
+      let call = client.get(request)
+      call.response.assertEqual(response, fulfill: responseExpectation)
+      call.status.map { $0.code }.assertEqual(.ok, fulfill: statusExpectation)
+    }
+    print("total time to send \(numberOfRequests) requests: \(Double(clock() - clockStart) / Double(CLOCKS_PER_SEC))")
+
+    self.wait(for: [responseExpectation, statusExpectation], timeout: self.defaultTestTimeout)
+    print("total time to receive \(numberOfRequests) responses: \(Double(clock() - clockStart) / Double(CLOCKS_PER_SEC))")
+  }
+
+  func testUnaryWithLargeData() throws {
+    // Default max frame size is: 16,384. We'll exceed this as we also have to send the size and compression flag.
+    let longMessage = String(repeating: "e", count: 16_384)
+    self.doTestUnary(message: longMessage)
+  }
+
+  func testUnaryEmptyRequest() throws {
+    self.doTestUnary(request: Echo_EchoRequest(), expect: Echo_EchoResponse(text: "Swift echo get: "))
+  }
+}
+
+extension NIOFunctionalTestsInsecureTransport {
+  func doTestClientStreaming(messages: [String], file: StaticString = #file, line: UInt = #line) throws {
+    let responseExpectation = self.makeResponseExpectation()
+    let statusExpectation = self.makeStatusExpectation()
+
+    let call = client.collect(callOptions: CallOptions(timeout: .infinite))
+    call.status.map { $0.code }.assertEqual(.ok, fulfill: statusExpectation, file: file, line: line)
+    call.response.assertEqual(Echo_EchoResponse(text: "Swift echo collect: \(messages.joined(separator: " "))"), fulfill: responseExpectation)
+
+    var queue = call.newMessageQueue()
+    for message in messages {
+      queue = queue.flatMap { call.sendMessage(Echo_EchoRequest(text: message)) }
+    }
+    queue.whenSuccess { call.sendEnd(promise: nil) }
+
+    self.wait(for: [responseExpectation, statusExpectation], timeout: self.defaultTestTimeout)
+  }
+
+  func testClientStreaming() {
+    XCTAssertNoThrow(try doTestClientStreaming(messages: aFewStrings))
+  }
+
+  func testClientStreamingLotsOfMessages() throws {
+    self.defaultTestTimeout = 15.0
+    XCTAssertNoThrow(try doTestClientStreaming(messages: lotsOfStrings))
+  }
+}
+
+extension NIOFunctionalTestsInsecureTransport {
+  func doTestServerStreaming(messages: [String], file: StaticString = #file, line: UInt = #line) throws {
+    let responseExpectation = self.makeResponseExpectation(expectedFulfillmentCount: messages.count)
+    let statusExpectation = self.makeStatusExpectation()
+
+    var iterator = messages.enumerated().makeIterator()
+    let call = client.expand(Echo_EchoRequest(text: messages.joined(separator: " "))) { response in
+      if let (index, message) = iterator.next() {
+        XCTAssertEqual(Echo_EchoResponse(text: "Swift echo expand (\(index)): \(message)"), response, file: file, line: line)
+        responseExpectation.fulfill()
+      } else {
+        XCTFail("Too many responses received", file: file, line: line)
+      }
+    }
+
+    call.status.map { $0.code }.assertEqual(.ok, fulfill: statusExpectation, file: file, line: line)
+    self.wait(for: [responseExpectation, statusExpectation], timeout: self.defaultTestTimeout)
+  }
+
+  func testServerStreaming() {
+    XCTAssertNoThrow(try doTestServerStreaming(messages: aFewStrings))
+  }
+
+  func testServerStreamingLotsOfMessages() {
+    self.defaultTestTimeout = 15.0
+    XCTAssertNoThrow(try doTestServerStreaming(messages: lotsOfStrings))
+  }
+}
+
+extension NIOFunctionalTestsInsecureTransport {
+  private func doTestBidirectionalStreaming(messages: [String], waitForEachResponse: Bool = false, file: StaticString = #file, line: UInt = #line) throws {
+    let responseExpectation = self.makeResponseExpectation(expectedFulfillmentCount: messages.count)
+    let statusExpectation = self.makeStatusExpectation()
+
+    let responseReceived = waitForEachResponse ? DispatchSemaphore(value: 0) : nil
+
+    var iterator = messages.enumerated().makeIterator()
+    let call = client.update { response in
+      if let (index, message) = iterator.next() {
+        XCTAssertEqual(Echo_EchoResponse(text: "Swift echo update (\(index)): \(message)"), response, file: file, line: line)
+        responseExpectation.fulfill()
+        responseReceived?.signal()
+      } else {
+        XCTFail("Too many responses received", file: file, line: line)
+      }
+    }
+
+    call.status.map { $0.code }.assertEqual(.ok, fulfill: statusExpectation, file: file, line: line)
+
+    messages.forEach { part in
+      call.sendMessage(Echo_EchoRequest(text: part), promise: nil)
+      XCTAssertNotEqual(responseReceived?.wait(timeout: .now() + .seconds(1)), .some(.timedOut), file: file, line: line)
+    }
+    call.sendEnd(promise: nil)
+
+    self.wait(for: [responseExpectation, statusExpectation], timeout: self.defaultTestTimeout)
+  }
+
+  func testBidirectionalStreamingBatched() throws {
+    XCTAssertNoThrow(try doTestBidirectionalStreaming(messages: aFewStrings))
+  }
+
+  func testBidirectionalStreamingPingPong() throws {
+    XCTAssertNoThrow(try doTestBidirectionalStreaming(messages: aFewStrings, waitForEachResponse: true))
+  }
+
+  func testBidirectionalStreamingLotsOfMessagesBatched() throws {
+    self.defaultTestTimeout = 15.0
+    XCTAssertNoThrow(try doTestBidirectionalStreaming(messages: lotsOfStrings))
+  }
+
+  func testBidirectionalStreamingLotsOfMessagesPingPong() throws {
+    self.defaultTestTimeout = 15.0
+    XCTAssertNoThrow(try doTestBidirectionalStreaming(messages: lotsOfStrings, waitForEachResponse: true))
+  }
+}
+
+class NIOFunctionalTestsAnonymousClient: NIOFunctionalTestsInsecureTransport {
+  override var transportSecurity: TransportSecurity {
+    return .anonymousClient
+  }
+}
+
+class NIOFunctionalTestsMutualAuthentication: NIOFunctionalTestsInsecureTransport {
+  override var transportSecurity: TransportSecurity {
+    return .mutualAuthentication
+  }
+}

+ 0 - 157
Tests/SwiftGRPCNIOTests/NIOServerTests.swift

@@ -1,157 +0,0 @@
-/*
- * Copyright 2018, 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.
- */
-import Dispatch
-import Foundation
-import NIO
-import NIOHTTP1
-import NIOHTTP2
-@testable import SwiftGRPCNIO
-import XCTest
-
-class NIOServerTests: NIOBasicEchoTestCase {
-  static var allTests: [(String, (NIOServerTests) -> () throws -> Void)] {
-    return [
-      ("testUnary", testUnary),
-      ("testUnaryLotsOfRequests", testUnaryLotsOfRequests),
-      ("testUnaryWithLargeData", testUnaryWithLargeData),
-      ("testUnaryEmptyRequest", testUnaryEmptyRequest),
-      ("testClientStreaming", testClientStreaming),
-      ("testClientStreamingLotsOfMessages", testClientStreamingLotsOfMessages),
-      ("testServerStreaming", testServerStreaming),
-      ("testServerStreamingLotsOfMessages", testServerStreamingLotsOfMessages),
-      ("testBidirectionalStreamingBatched", testBidirectionalStreamingBatched),
-      ("testBidirectionalStreamingPingPong", testBidirectionalStreamingPingPong),
-      ("testBidirectionalStreamingLotsOfMessagesBatched", testBidirectionalStreamingLotsOfMessagesBatched),
-      ("testBidirectionalStreamingLotsOfMessagesPingPong", testBidirectionalStreamingLotsOfMessagesPingPong)
-    ]
-  }
-
-  static let aFewStrings = ["foo", "bar", "baz"]
-  static let lotsOfStrings = (0..<5_000).map { String(describing: $0) }
-}
-
-extension NIOServerTests {
-  func testUnary() throws {
-    XCTAssertEqual(try client.get(Echo_EchoRequest(text: "foo")).response.wait().text, "Swift echo get: foo")
-  }
-
-  func testUnaryLotsOfRequests() throws {
-    // Sending that many requests at once can sometimes trip things up, it seems.
-    let clockStart = clock()
-    let numberOfRequests = 2_000
-
-    for i in 0..<numberOfRequests {
-      if i % 1_000 == 0 && i > 0 {
-        print("\(i) requests sent so far, elapsed time: \(Double(clock() - clockStart) / Double(CLOCKS_PER_SEC))")
-      }
-      XCTAssertEqual(try client.get(Echo_EchoRequest(text: "foo \(i)")).response.wait().text, "Swift echo get: foo \(i)")
-    }
-    print("total time for \(numberOfRequests) requests: \(Double(clock() - clockStart) / Double(CLOCKS_PER_SEC))")
-  }
-
-  func testUnaryWithLargeData() throws {
-    // Default max frame size is: 16,384. We'll exceed this as we also have to send the size and compression flag.
-    let longMessage = String(repeating: "e", count: 16_384)
-    XCTAssertEqual(try client.get(Echo_EchoRequest(text: longMessage)).response.wait().text, "Swift echo get: \(longMessage)")
-  }
-
-  func testUnaryEmptyRequest() throws {
-    XCTAssertNoThrow(try client.get(Echo_EchoRequest()).response.wait())
-  }
-}
-
-extension NIOServerTests {
-  func doTestClientStreaming(messages: [String], file: StaticString = #file, line: UInt = #line) throws {
-    let call = client.collect(callOptions: CallOptions(timeout: .infinite))
-
-    var queue = call.newMessageQueue()
-    for message in messages {
-      queue = queue.flatMap { call.sendMessage(Echo_EchoRequest(text: message)) }
-    }
-    queue.whenSuccess { call.sendEnd(promise: nil) }
-
-    XCTAssertEqual("Swift echo collect: " + messages.joined(separator: " "), try call.response.wait().text, file: file, line: line)
-    XCTAssertEqual(.ok, try call.status.wait().code, file: file, line: line)
-  }
-
-  func testClientStreaming() {
-    XCTAssertNoThrow(try doTestClientStreaming(messages: NIOServerTests.aFewStrings))
-  }
-
-  func testClientStreamingLotsOfMessages() throws {
-    XCTAssertNoThrow(try doTestClientStreaming(messages: NIOServerTests.lotsOfStrings))
-  }
-}
-
-extension NIOServerTests {
-  func doTestServerStreaming(messages: [String], file: StaticString = #file, line: UInt = #line) throws {
-    var index = 0
-    let call = client.expand(Echo_EchoRequest.with { $0.text = messages.joined(separator: " ") }) { response in
-      XCTAssertEqual("Swift echo expand (\(index)): \(messages[index])", response.text, file: file, line: line)
-      index += 1
-    }
-
-    XCTAssertEqual(try call.status.wait().code, .ok, file: file, line: line)
-    XCTAssertEqual(index, messages.count)
-  }
-
-  func testServerStreaming() {
-    XCTAssertNoThrow(try doTestServerStreaming(messages: NIOServerTests.aFewStrings))
-  }
-
-  func testServerStreamingLotsOfMessages() {
-    XCTAssertNoThrow(try doTestServerStreaming(messages: NIOServerTests.lotsOfStrings))
-  }
-}
-
-extension NIOServerTests {
-  private func doTestBidirectionalStreaming(messages: [String], waitForEachResponse: Bool = false, timeout: GRPCTimeout? = nil, file: StaticString = #file, line: UInt = #line) throws {
-    let responseReceived = waitForEachResponse ? DispatchSemaphore(value: 0) : nil
-    var index = 0
-
-    let callOptions = timeout.map { CallOptions(timeout: $0) }
-    let call = client.update(callOptions: callOptions) { response in
-      XCTAssertEqual("Swift echo update (\(index)): \(messages[index])", response.text, file: file, line: line)
-      responseReceived?.signal()
-      index += 1
-    }
-
-    messages.forEach { part in
-      call.sendMessage(Echo_EchoRequest(text: part), promise: nil)
-      XCTAssertNotEqual(responseReceived?.wait(timeout: .now() + .seconds(1)), .some(.timedOut), file: file, line: line)
-    }
-    call.sendEnd(promise: nil)
-
-    XCTAssertEqual(try call.status.wait().code, .ok, file: file, line: line)
-    XCTAssertEqual(index, messages.count)
-  }
-
-  func testBidirectionalStreamingBatched() throws {
-    XCTAssertNoThrow(try doTestBidirectionalStreaming(messages: NIOServerTests.aFewStrings))
-  }
-
-  func testBidirectionalStreamingPingPong() throws {
-    XCTAssertNoThrow(try doTestBidirectionalStreaming(messages: NIOServerTests.aFewStrings, waitForEachResponse: true))
-  }
-
-  func testBidirectionalStreamingLotsOfMessagesBatched() throws {
-    XCTAssertNoThrow(try doTestBidirectionalStreaming(messages: NIOServerTests.lotsOfStrings, timeout: try .seconds(15)))
-  }
-
-  func testBidirectionalStreamingLotsOfMessagesPingPong() throws {
-    XCTAssertNoThrow(try doTestBidirectionalStreaming(messages: NIOServerTests.lotsOfStrings, waitForEachResponse: true, timeout: try .seconds(15)))
-  }
-}

+ 1 - 1
Tests/SwiftGRPCNIOTests/NIOServerWebTests.swift

@@ -21,7 +21,7 @@ import XCTest
 // Only test Unary and ServerStreaming, as ClientStreaming is not
 // supported in HTTP1.
 // TODO: Add tests for application/grpc-web as well.
-class NIOServerWebTests: NIOBasicEchoTestCase {
+class NIOServerWebTests: NIOEchoTestCaseBase {
   static var allTests: [(String, (NIOServerWebTests) -> () throws -> Void)] {
     return [
       ("testUnary", testUnary),

+ 54 - 0
Tests/SwiftGRPCNIOTests/RawGRPCServerResponsePart+Assertions.swift

@@ -0,0 +1,54 @@
+/*
+ * Copyright 2019, 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.
+ */
+import Foundation
+import SwiftGRPCNIO
+import NIOHTTP1
+import XCTest
+
+extension RawGRPCServerResponsePart {
+  /// Asserts that this value represents the headers case.
+  ///
+  /// - Parameter validate: A block to further validate the headers.
+  func assertHeaders(validate: ((HTTPHeaders) -> Void)? = nil) {
+    guard case .headers(let headers) = self else {
+      XCTFail("Expected .headers but got \(self)")
+      return
+    }
+    validate?(headers)
+  }
+
+  /// Asserts that this value represents the message case.
+  ///
+  /// - Parameter validate: A block to further validate the message.
+  func assertMessage(validate: ((Data) -> Void)? = nil) {
+    guard case .message(let message) = self else {
+      XCTFail("Expected .message but got \(self)")
+      return
+    }
+    validate?(message)
+  }
+
+  /// Asserts that this value represents the status case.
+  ///
+  /// - Parameter validate: A block to further validate the status.
+  func assertStatus(validate: ((GRPCStatus) -> Void)? = nil) {
+    guard case .status(let status) = self else {
+      XCTFail("Expected .status but got \(self)")
+      return
+    }
+    validate?(status)
+  }
+}

+ 24 - 0
Tests/SwiftGRPCNIOTests/SampleCertificate+Assertions.swift

@@ -0,0 +1,24 @@
+/*
+ * Copyright 2019, 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.
+ */
+import Foundation
+import XCTest
+import SwiftGRPCNIOSampleData
+
+extension SampleCertificate {
+  func assertNotExpired(file: StaticString = #file, line: UInt = #line) {
+    XCTAssertFalse(self.isExpired, "Certificate expired at \(self.notAfter)", file: file, line: line)
+  }
+}

+ 1 - 1
Tests/SwiftGRPCNIOTests/ServerThrowingTests.swift

@@ -93,7 +93,7 @@ private class ErrorReturningEchoProviderNIO: ImmediateThrowingEchoProviderNIO {
   }
 }
 
-class ServerThrowingTests: NIOBasicEchoTestCase {
+class ServerThrowingTests: NIOEchoTestCaseBase {
   override func makeEchoProvider() -> Echo_EchoProvider_NIO { return ImmediateThrowingEchoProviderNIO() }
   
   static var allTests: [(String, (ServerThrowingTests) -> () throws -> Void)] {

+ 0 - 61
Tests/SwiftGRPCNIOTests/TestHelpers.swift

@@ -1,61 +0,0 @@
-/*
- * Copyright 2019, 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.
- */
-import Foundation
-import XCTest
-import SwiftGRPCNIO
-import NIO
-import NIOHTTP1
-
-// Assert the given expression does not throw, and validate the return value from that expression.
-public func XCTAssertNoThrow<T>(
-  _ expression: @autoclosure () throws -> T,
-  _ message: String = "",
-  file: StaticString = #file,
-  line: UInt = #line,
-  validate: (T) -> Void
-) {
-  var value: T? = nil
-  XCTAssertNoThrow(try value = expression(), message, file: file, line: line)
-  value.map { validate($0) }
-}
-
-struct CaseExtractError: Error {
-  let message: String
-}
-
-@discardableResult
-func extractHeaders(_ response: RawGRPCServerResponsePart) throws -> HTTPHeaders {
-  guard case .headers(let headers) = response else {
-    throw CaseExtractError(message: "\(response) did not match .headers")
-  }
-  return headers
-}
-
-@discardableResult
-func extractMessage(_ response: RawGRPCServerResponsePart) throws -> Data {
-  guard case .message(let message) = response else {
-    throw CaseExtractError(message: "\(response) did not match .message")
-  }
-  return message
-}
-
-@discardableResult
-func extractStatus(_ response: RawGRPCServerResponsePart) throws -> GRPCStatus {
-  guard case .status(let status) = response else {
-    throw CaseExtractError(message: "\(response) did not match .status")
-  }
-  return status
-}