packages feed

grpc-haskell-core-0.0.0.0: tests/LowLevelTests.hs

{-# LANGUAGE DataKinds                  #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase                 #-}
{-# LANGUAGE OverloadedLists            #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE PatternSynonyms            #-}
{-# LANGUAGE RecordWildCards            #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-# LANGUAGE TupleSections              #-}
{-# OPTIONS_GHC -fno-warn-orphans       #-}

module LowLevelTests where

import           Control.Concurrent                        (threadDelay)
import           Control.Concurrent.Async
import           Control.Monad
import           Control.Monad.Managed
import           Data.ByteString                           (ByteString,
                                                            isPrefixOf,
                                                            isSuffixOf)
import           Data.List                                 (find)
import qualified Data.Map.Strict                           as M
import qualified Data.Set                                  as S
import           GHC.Exts                                  (fromList, toList)
import           Network.GRPC.Unsafe.ChannelArgs           (Arg(..))
import           Network.GRPC.LowLevel
import qualified Network.GRPC.LowLevel.Call.Unregistered   as U
import qualified Network.GRPC.LowLevel.Client.Unregistered as U
import           Network.GRPC.LowLevel.GRPC                (threadDelaySecs)
import qualified Network.GRPC.LowLevel.Server.Unregistered as U
import qualified Pipes                                     as P
import           Test.Tasty
import           Test.Tasty.HUnit                          as HU (Assertion,
                                                                  assertBool,
                                                                  assertEqual,
                                                                  assertFailure,
                                                                  testCase,
                                                                  (@?=))

lowLevelTests :: TestTree
lowLevelTests = testGroup "Unit tests of low-level Haskell library"
  [ testGRPCBracket
  , testCompletionQueueCreateDestroy
  , testClientCreateDestroy
  , testClientCall
  , testClientTimeoutNoServer
  , testServerCreateDestroy
  , testMixRegisteredUnregistered
  , testPayload
  , testSSL
  , testAuthMetadataTransfer
  , testServerAuthProcessorCancel
  , testPayloadUnregistered
  , testServerCancel
  , testGoaway
  , testSlowServer
  , testServerCallExpirationCheck
  , testCustomUserAgent
  , testClientCompression
  , testClientServerCompression
  , testClientMaxReceiveMessageLengthChannelArg
  , testClientStreaming
  , testClientStreamingUnregistered
  , testServerStreaming
  , testServerStreamingUnregistered
  , testBiDiStreaming
  , testBiDiStreamingUnregistered
  ]

testGRPCBracket :: TestTree
testGRPCBracket =
  testCase "Start/stop GRPC" $ runManaged $ void mgdGRPC

testCompletionQueueCreateDestroy :: TestTree
testCompletionQueueCreateDestroy =
  testCase "Create/destroy CQ" $ runManaged $ do
    g <- mgdGRPC
    liftIO (withCompletionQueue g nop)

testClientCreateDestroy :: TestTree
testClientCreateDestroy =
  clientOnlyTest "start/stop" nop

testClientTimeoutNoServer :: TestTree
testClientTimeoutNoServer =
  clientOnlyTest "request timeout when server DNE" $ \c -> do
    rm <- clientRegisterMethodNormal c "/foo"
    r  <- clientRequest c rm 1 "Hello" mempty
    r @?= Left GRPCIOTimeout

testServerCreateDestroy :: TestTree
testServerCreateDestroy =
  serverOnlyTest "start/stop" (["/foo"],[],[],[]) nop

testMixRegisteredUnregistered :: TestTree
testMixRegisteredUnregistered =
  csTest "server uses unregistered calls to handle unknown endpoints"
         client
         server
         (["/foo"],[],[],[])
  where
    client c = do
      rm1 <- clientRegisterMethodNormal c "/foo"
      rm2 <- clientRegisterMethodNormal c "/bar"
      clientRequest c rm1 1 "Hello" mempty >>= do
        checkReqRslt $ \NormalRequestResult{..} -> do
          rspBody @?= "reply test"
          initMD @?= dummyMeta
          trailMD @?= dummyMeta
      clientRequest c rm2 1 "bad endpoint" mempty >>= do
        checkReqRslt $ \NormalRequestResult{..} -> do
          rspBody @?= ""
      return ()
    server s = do
       concurrently regThread unregThread
       return ()
       where regThread = do
               let rm = head (normalMethods s)
               _r <- serverHandleNormalCall s rm dummyMeta $ \c -> do
                 payload c @?= "Hello"
                 return ("reply test", dummyMeta, StatusOk, "")
               return ()
             unregThread = do
               U.serverHandleNormalCall s mempty $ \call _ -> do
                 U.callMethod call @?= "/bar"
                 return ("", mempty, StatusOk,
                         StatusDetails "Wrong endpoint")
               return ()

-- TODO: There seems to be a race here (and in other client/server pairs, of
-- course) about what gets reported when there is a failure. E.g., if one of the
-- Assertions fails in the request processing block for the server, we /may/ get
-- that error reported accurately as a call cancellation on the client, rather
-- than a useful error about the failure on the server. Maybe we'll need to
-- tweak EH behavior / async use.
testPayload :: TestTree
testPayload =
  csTest "registered normal request/response" client server (["/foo"],[],[],[])
  where
    clientMD = [ ("foo_key", "foo_val")
               , ("bar_key", "bar_val")
               , ("bar_key", "bar_repeated_val")]
    client c = do
      rm <- clientRegisterMethodNormal c "/foo"
      clientRequest c rm 10 "Hello!" clientMD >>= do
        checkReqRslt $ \NormalRequestResult{..} -> do
          rspCode @?= StatusOk
          rspBody @?= "reply test"
          details @?= "details string"
          initMD  @?= dummyMeta
          trailMD @?= dummyMeta
    server s = do
      let rm = head (normalMethods s)
      r <- serverHandleNormalCall s rm dummyMeta $ \c -> do
        payload c @?= "Hello!"
        checkMD "Server metadata mismatch" clientMD (metadata c)
        return ("reply test", dummyMeta, StatusOk, "details string")
      r @?= Right ()

testSSL :: TestTree
testSSL =
  csTest' "request/response using SSL" client server
  where
    clientConf = stdClientConf
                 {clientSSLConfig = Just (ClientSSLConfig
                                            (Just "tests/ssl/localhost.crt")
                                            Nothing
                                            Nothing)
                }
    client = TestClient clientConf $ \c -> do
      rm <- clientRegisterMethodNormal c "/foo"
      clientRequest c rm 10 "hi" mempty >>= do
        checkReqRslt $ \NormalRequestResult{..} -> do
          rspCode @?= StatusOk
          rspBody @?= "reply test"

    serverConf' = defServerConf
                  { sslConfig = Just (ServerSSLConfig
                                        Nothing
                                        "tests/ssl/localhost.key"
                                        "tests/ssl/localhost.crt"
                                        SslDontRequestClientCertificate
                                        Nothing)
                  }
    server = TestServer serverConf' $ \s -> do
      r <- U.serverHandleNormalCall s mempty $ \U.ServerCall{..} body -> do
        body @?= "hi"
        return ("reply test", mempty, StatusOk, "")
      r @?= Right ()

-- NOTE: With auth plugin tests, if an auth plugin decides someone isn't
-- authenticated, then the call never happens from the perspective of
-- the server, so the server will continue to block waiting for a call. So, if
-- these tests hang forever, it's because auth failed and the server is still
-- waiting for a successfully authenticated call to come in.

testServerAuthProcessorCancel :: TestTree
testServerAuthProcessorCancel =
  csTest' "request rejection by auth processor" client server
  where
    clientConf = stdClientConf
                 {clientSSLConfig = Just (ClientSSLConfig
                                            (Just "tests/ssl/localhost.crt")
                                            Nothing
                                            Nothing)
                }
    client = TestClient clientConf $ \c -> do
      rm <- clientRegisterMethodNormal c "/foo"
      r <- clientRequest c rm 10 "hi" mempty
      -- TODO: using checkReqRslt on this first result causes the test to hang!
      r @?= Left (GRPCIOBadStatusCode StatusUnauthenticated "denied!")
      clientRequest c rm 10 "hi" [("foo","bar")] >>= do
        checkReqRslt $ \NormalRequestResult{..} -> do
          rspCode @?= StatusOk
          rspBody @?= "reply test"

    serverProcessor = Just $ \_ m -> do
      let (status, details) = if M.member "foo" (unMap m)
                                 then (StatusOk, "")
                                 else (StatusUnauthenticated, "denied!")
      return $ AuthProcessorResult mempty mempty status details

    serverConf' = defServerConf
                  { sslConfig = Just (ServerSSLConfig
                                        Nothing
                                        "tests/ssl/localhost.key"
                                        "tests/ssl/localhost.crt"
                                        SslDontRequestClientCertificate
                                        serverProcessor)
                  }
    server = TestServer serverConf' $ \s -> do
      r <- U.serverHandleNormalCall s mempty $ \U.ServerCall{..} _body -> do
        checkMD "Handler only sees requests with good metadata"
                [("foo","bar")]
                metadata
        return ("reply test", mempty, StatusOk, "")
      r @?= Right ()

testAuthMetadataTransfer :: TestTree
testAuthMetadataTransfer =
  csTest' "Auth metadata changes sent from client to server" client server
  where
    plugin :: ClientMetadataCreate
    plugin authMetaCtx = do
      let authCtx = (channelAuthContext authMetaCtx)

      addAuthProperty authCtx (AuthProperty "foo1" "bar1")
      newProps <- getAuthProperties authCtx
      let addedProp = find ((== "foo1") . authPropName) newProps
      addedProp @?= Just (AuthProperty "foo1" "bar1")
      return $ ClientMetadataCreateResult [("foo","bar")] StatusOk ""
    clientConf = stdClientConf
                 {clientSSLConfig = Just (ClientSSLConfig
                                            (Just "tests/ssl/localhost.crt")
                                            Nothing
                                            (Just plugin))
                }
    client = TestClient clientConf $ \c -> do
      rm <- clientRegisterMethodNormal c "/foo"
      clientRequest c rm 10 "hi" mempty >>= do
        checkReqRslt $ \NormalRequestResult{..} -> do
          rspCode @?= StatusOk
          rspBody @?= "reply test"

    serverProcessor :: Maybe ProcessMeta
    serverProcessor = Just $ \authCtx m -> do
      let expected = fromList [("foo","bar")]

      props <- getAuthProperties authCtx
      let clientProp = find ((== "foo1") . authPropName) props
      assertBool "server plugin doesn't see auth properties set by client"
                 (clientProp == Nothing)
      checkMD "server plugin sees metadata added by client plugin" expected m
      return $ AuthProcessorResult mempty mempty StatusOk ""

    serverConf' = defServerConf
                  { sslConfig = Just (ServerSSLConfig
                                        Nothing
                                        "tests/ssl/localhost.key"
                                        "tests/ssl/localhost.crt"
                                        SslDontRequestClientCertificate
                                        serverProcessor)
                  }
    server = TestServer serverConf' $ \s -> do
      r <- U.serverHandleNormalCall s mempty $ \U.ServerCall{..} body -> do
        body @?= "hi"
        return ("reply test", mempty, StatusOk, "")
      r @?= Right ()

-- TODO: auth metadata doesn't propagate from parent calls to child calls.
-- Once we implement our own system for doing so, update this test and add it
-- to the tests list.
testAuthMetadataPropagate :: TestTree
testAuthMetadataPropagate = testCase "auth metadata inherited by children" $ do
  c <- async client
  s1 <- async server
  s2 <- async server2
  wait c
  wait s1
  wait s2
  return ()
  where
    clientPlugin _ =
      return $ ClientMetadataCreateResult [("foo","bar")] StatusOk ""
    clientConf = stdClientConf
                 {clientSSLConfig = Just (ClientSSLConfig
                                            (Just "tests/ssl/localhost.crt")
                                            Nothing
                                            (Just clientPlugin))
                }
    client = do
      threadDelaySecs 3
      withGRPC $ \g -> withClient g clientConf $ \c -> do
        rm <- clientRegisterMethodNormal c "/foo"
        clientRequest c rm 10 "hi" mempty >>= do
          checkReqRslt $ \NormalRequestResult{..} -> do
            rspCode @?= StatusOk
            rspBody @?= "reply test"

    server1ServerPlugin _ctx md = do
      checkMD "server1 sees client's auth metadata." [("foo","bar")] md
      -- TODO: add response meta to check, and consume meta to see what happens.
      return $ AuthProcessorResult mempty mempty StatusOk ""

    server1ServerConf = defServerConf
                 {sslConfig = Just (ServerSSLConfig
                                      Nothing
                                      "tests/ssl/localhost.key"
                                      "tests/ssl/localhost.crt"
                                      SslDontRequestClientCertificate
                                      (Just server1ServerPlugin)),
                  methodsToRegisterNormal = ["/foo"]
                }

    server1ClientPlugin _ =
      return $ ClientMetadataCreateResult [("foo1","bar1")] StatusOk ""

    server1ClientConf = stdClientConf
                 {clientSSLConfig = Just (ClientSSLConfig
                                            (Just "tests/ssl/localhost.crt")
                                            Nothing
                                            (Just server1ClientPlugin)),
                  clientServerPort = 50052
                }

    server = do
      threadDelaySecs 2
      withGRPC $ \g -> withServer g server1ServerConf $ \s ->
        withClient g server1ClientConf $ \c -> do
          let rm = head (normalMethods s)
          serverHandleNormalCall s rm mempty $ \call -> do
            rmc <- clientRegisterMethodNormal c "/foo"
            res <- clientRequestParent c (Just call) rmc 10 "hi" mempty
            case res of
              Left _ ->
                error "got bad result from server2"
              Right (NormalRequestResult{..}) ->
                return (rspBody, mempty, StatusOk, "")

    server2ServerPlugin _ctx md = do
      print md
      checkMD "server2 sees server1's auth metadata." [("foo1","bar1")] md
      --TODO: this assert fails
      checkMD "server2 sees client's auth metadata." [("foo","bar")] md
      return $ AuthProcessorResult mempty mempty StatusOk ""

    server2ServerConf = defServerConf
                 {sslConfig = Just (ServerSSLConfig
                                      Nothing
                                      "tests/ssl/localhost.key"
                                      "tests/ssl/localhost.crt"
                                      SslDontRequestClientCertificate
                                      (Just server2ServerPlugin)),
                  methodsToRegisterNormal = ["/foo"],
                  port = 50052
                }

    server2 = withGRPC $ \g -> withServer g server2ServerConf $ \s -> do
        let rm = head (normalMethods s)
        serverHandleNormalCall s rm mempty $ \_call -> do
          return ("server2 reply", mempty, StatusOk, "")

testServerCancel :: TestTree
testServerCancel =
  csTest "server cancel call" client server (["/foo"],[],[],[])
  where
    client c = do
      rm <- clientRegisterMethodNormal c "/foo"
      Left (GRPCIOBadStatusCode s _) <- clientRequest c rm 10 "" mempty
      s @?= StatusCancelled
    server s = do
      let rm = head (normalMethods s)
      r <- serverHandleNormalCall s rm mempty $ \c -> do
        serverCallCancel c StatusCancelled ""
        return (mempty, mempty, StatusCancelled, "")
      r @?= Right ()

testServerStreaming :: TestTree
testServerStreaming =
  csTest "server streaming" client server ([],[],["/feed"],[])
  where
    clientInitMD = [("client","initmd")]
    serverInitMD = [("server","initmd")]
    clientPay    = "FEED ME!"
    pays         = ["ONE", "TWO", "THREE", "FOUR"] :: [ByteString]

    client c = do
      rm <- clientRegisterMethodServerStreaming c "/feed"
      eea <- clientReader c rm 10 clientPay clientInitMD $ \_cc initMD recv -> do
        checkMD "Server initial metadata mismatch" serverInitMD initMD
        forM_ pays $ \p -> recv `is` Right (Just p)
        recv `is` Right Nothing
      eea @?= Right (dummyMeta, StatusOk, "dtls")

    server s = do
      let rm = head (sstreamingMethods s)
      r <- serverWriter s rm serverInitMD $ \sc send -> do
        checkMD "Server request metadata mismatch" clientInitMD (metadata sc)
        payload sc @?= clientPay
        forM_ pays $ \p -> send p `is` Right ()
        return (dummyMeta, StatusOk, "dtls")
      r @?= Right ()

-- TODO: these unregistered streaming tests are basically the same as the
-- registered ones. Reduce duplication.
-- TODO: Once client-side unregistered streaming functions are added, switch
-- to using them in these tests.
testServerStreamingUnregistered :: TestTree
testServerStreamingUnregistered =
  csTest "unregistered server streaming" client server ([],[],[],[])
  where
    clientInitMD = [("client","initmd")]
    serverInitMD = [("server","initmd")]
    clientPay    = "FEED ME!"
    pays         = ["ONE", "TWO", "THREE", "FOUR"] :: [ByteString]

    client c = do
      rm <- clientRegisterMethodServerStreaming c "/feed"
      eea <- clientReader c rm 10 clientPay clientInitMD $ \_cc initMD recv -> do
        checkMD "Server initial metadata mismatch" serverInitMD initMD
        forM_ pays $ \p -> recv `is` Right (Just p)
        recv `is` Right Nothing
      eea @?= Right (dummyMeta, StatusOk, "dtls")

    server s = U.withServerCallAsync s $ \call -> do
      r <- U.serverWriter s call serverInitMD $ \sc send -> do
        checkMD "Server request metadata mismatch" clientInitMD (metadata sc)
        payload sc @?= clientPay
        forM_ pays $ \p -> send p `is` Right ()
        return (dummyMeta, StatusOk, "dtls")
      r @?= Right ()

testClientStreaming :: TestTree
testClientStreaming =
  csTest "client streaming" client server ([],["/slurp"],[],[])
  where
    clientInitMD = [("a","b")]
    serverInitMD = [("x","y")]
    trailMD      = dummyMeta
    serverRsp    = "serverReader reply"
    serverDtls   = "deets"
    serverStatus = StatusOk
    pays         = ["P_ONE", "P_TWO", "P_THREE"] :: [ByteString]

    client c = do
      rm  <- clientRegisterMethodClientStreaming c "/slurp"
      eea <- clientWriter c rm 10 clientInitMD $ \send -> do
        -- liftIO $ checkMD "Server initial metadata mismatch" serverInitMD initMD
        forM_ pays $ \p -> send p `is` Right ()
      eea @?= Right (Just serverRsp, serverInitMD, trailMD, serverStatus, serverDtls)

    server s = do
      let rm = head (cstreamingMethods s)
      eea <- serverReader s rm serverInitMD $ \sc recv -> do
        checkMD "Client request metadata mismatch" clientInitMD (metadata sc)
        forM_ pays $ \p -> recv `is` Right (Just p)
        recv `is` Right Nothing
        return (Just serverRsp, trailMD, serverStatus, serverDtls)
      eea @?= Right ()

testClientStreamingUnregistered :: TestTree
testClientStreamingUnregistered =
  csTest "unregistered client streaming" client server ([],[],[],[])
  where
    clientInitMD = [("a","b")]
    serverInitMD = [("x","y")]
    trailMD      = dummyMeta
    serverRsp    = "serverReader reply"
    serverDtls   = "deets"
    serverStatus = StatusOk
    pays         = ["P_ONE", "P_TWO", "P_THREE"] :: [ByteString]

    client c = do
      rm  <- clientRegisterMethodClientStreaming c "/slurp"
      eea <- clientWriter c rm 10 clientInitMD $ \send -> do
        -- liftIO $ checkMD "Server initial metadata mismatch" serverInitMD initMD
        forM_ pays $ \p -> send p `is` Right ()
      eea @?= Right (Just serverRsp, serverInitMD, trailMD, serverStatus, serverDtls)

    server s = U.withServerCallAsync s $ \call -> do
      eea <- U.serverReader s call serverInitMD $ \sc recv -> do
        checkMD "Client request metadata mismatch" clientInitMD (metadata sc)
        forM_ pays $ \p -> recv `is` Right (Just p)
        recv `is` Right Nothing
        return (Just serverRsp, trailMD, serverStatus, serverDtls)
      eea @?= Right ()

testBiDiStreaming :: TestTree
testBiDiStreaming =
  csTest "bidirectional streaming" client server ([],[],[],["/bidi"])
  where
    clientInitMD = [("bidi-streaming","client")]
    serverInitMD = [("bidi-streaming","server")]
    trailMD      = dummyMeta
    serverStatus = StatusOk
    serverDtls   = "deets"

    client c = do
      rm  <- clientRegisterMethodBiDiStreaming c "/bidi"
      eea <- clientRW c rm 10 clientInitMD $ \_cc getMD recv send writesDone -> do
        either clientFail (checkMD "Server rsp metadata mismatch" serverInitMD) =<< getMD
        send "cw0" `is` Right ()
        recv       `is` Right (Just "sw0")
        send "cw1" `is` Right ()
        recv       `is` Right (Just "sw1")
        recv       `is` Right (Just "sw2")
        writesDone `is` Right ()
        recv       `is` Right Nothing
      eea @?= Right (trailMD, serverStatus, serverDtls)

    server s = do
      let rm = head (bidiStreamingMethods s)
      eea <- serverRW s rm serverInitMD $ \sc recv send -> do
        checkMD "Client request metadata mismatch" clientInitMD (metadata sc)
        recv       `is` Right (Just "cw0")
        send "sw0" `is` Right ()
        recv       `is` Right (Just "cw1")
        send "sw1" `is` Right ()
        send "sw2" `is` Right ()
        recv       `is` Right Nothing
        return (trailMD, serverStatus, serverDtls)
      eea @?= Right ()

testBiDiStreamingUnregistered :: TestTree
testBiDiStreamingUnregistered =
  csTest "unregistered bidirectional streaming" client server ([],[],[],[])
  where
    clientInitMD = [("bidi-streaming","client")]
    serverInitMD = [("bidi-streaming","server")]
    trailMD      = dummyMeta
    serverStatus = StatusOk
    serverDtls   = "deets"

    client c = do
      rm  <- clientRegisterMethodBiDiStreaming c "/bidi"
      eea <- clientRW c rm 10 clientInitMD $ \_cc getMD recv send writesDone -> do
        either clientFail (checkMD "Server rsp metadata mismatch" serverInitMD) =<< getMD
        send "cw0" `is` Right ()
        recv       `is` Right (Just "sw0")
        send "cw1" `is` Right ()
        recv       `is` Right (Just "sw1")
        recv       `is` Right (Just "sw2")
        writesDone `is` Right ()
        recv       `is` Right Nothing
      eea @?= Right (trailMD, serverStatus, serverDtls)

    server s = U.withServerCallAsync s $ \call -> do
      eea <- U.serverRW s call serverInitMD $ \sc recv send -> do
        checkMD "Client request metadata mismatch" clientInitMD (metadata sc)
        recv       `is` Right (Just "cw0")
        send "sw0" `is` Right ()
        recv       `is` Right (Just "cw1")
        send "sw1" `is` Right ()
        send "sw2" `is` Right ()
        recv       `is` Right Nothing
        return (trailMD, serverStatus, serverDtls)
      eea @?= Right ()

--------------------------------------------------------------------------------
-- Unregistered tests

testClientCall :: TestTree
testClientCall =
  clientOnlyTest "create/destroy call" $ \c -> do
    r <- U.withClientCall c "/foo" 10 $ const $ return $ Right ()
    r @?= Right ()

testServerCall :: TestTree
testServerCall =
  serverOnlyTest "create/destroy call" ([],[],[],[]) $ \s -> do
    r <- U.withServerCall s $ const $ return $ Right ()
    r @?= Left GRPCIOTimeout

testPayloadUnregistered :: TestTree
testPayloadUnregistered =
  csTest "unregistered normal request/response" client server ([],[],[],[])
  where
    client c =
      U.clientRequest c "/foo" 10 "Hello!" mempty >>= do
        checkReqRslt $ \NormalRequestResult{..} -> do
          rspCode @?= StatusOk
          rspBody @?= "reply test"
          details @?= "details string"
    server s = do
      r <- U.serverHandleNormalCall s mempty $ \U.ServerCall{..} body -> do
             body @?= "Hello!"
             callMethod @?= "/foo"
             return ("reply test", mempty, StatusOk, "details string")
      r @?= Right ()

testGoaway :: TestTree
testGoaway =
  csTest "Client handles server shutdown gracefully"
         client
         server
         (["/foo"],[],[],[])
  where
    client c = do
      rm <- clientRegisterMethodNormal c "/foo"
      clientRequest c rm 10 "" mempty
      clientRequest c rm 10 "" mempty
      eer <- clientRequest c rm 1 "" mempty
      assertBool "Client handles server shutdown gracefully" $ case eer of
        Left (GRPCIOBadStatusCode StatusUnavailable _)                        -> True
        Left (GRPCIOBadStatusCode StatusDeadlineExceeded "Deadline Exceeded") -> True
        Left GRPCIOTimeout                                                    -> True
        _                                                                     -> False

    server s = do
      let rm = head (normalMethods s)
      serverHandleNormalCall s rm mempty dummyHandler
      serverHandleNormalCall s rm mempty dummyHandler
      return ()

testSlowServer :: TestTree
testSlowServer =
  csTest "Client handles slow server response" client server (["/foo"],[],[],[])
  where
    client c = do
      rm <- clientRegisterMethodNormal c "/foo"
      result <- clientRequest c rm 1 "" mempty
      result @?= Left (GRPCIOBadStatusCode StatusDeadlineExceeded "Deadline Exceeded")
    server s = do
      let rm = head (normalMethods s)
      serverHandleNormalCall s rm mempty $ \_ -> do
        threadDelay (2*10^(6 :: Int))
        return dummyResp
      return ()

testServerCallExpirationCheck :: TestTree
testServerCallExpirationCheck =
  csTest "Check for call expiration" client server (["/foo"],[],[],[])
  where
    client c = do
      rm <- clientRegisterMethodNormal c "/foo"
      void $ clientRequest c rm 3 "" mempty
    server s = do
      let rm = head (normalMethods s)
      serverHandleNormalCall s rm mempty $ \c -> do
        exp1 <- serverCallIsExpired c
        assertBool "Call isn't expired when handler starts" $ not exp1
        threadDelaySecs 1
        exp2 <- serverCallIsExpired c
        assertBool "Call isn't expired after 1 second" $ not exp2
        threadDelaySecs 3
        exp3 <- serverCallIsExpired c
        assertBool "Call is expired after 4 seconds" exp3
        return dummyResp
      return ()

testCustomUserAgent :: TestTree
testCustomUserAgent =
  csTest' "Server sees custom user agent prefix/suffix" client server
  where
    clientArgs = [UserAgentPrefix "prefix!", UserAgentSuffix "suffix!"]
    client =
      TestClient (ClientConfig "localhost" 50051 clientArgs Nothing Nothing) $
        \c -> do rm <- clientRegisterMethodNormal c "/foo"
                 void $ clientRequest c rm 4 "" mempty
    server = TestServer (serverConf (["/foo"],[],[],[])) $ \s -> do
      let rm = head (normalMethods s)
      serverHandleNormalCall s rm mempty $ \c -> do
        ua <- case toList $ (unMap $ metadata c) M.! "user-agent" of
                []   -> fail "user-agent missing from metadata."
                [ua] -> return ua
                _    -> fail "multiple user-agent keys."
        assertBool "User agent prefix is present" $ isPrefixOf "prefix!" ua
        assertBool "User agent suffix is present" $ isSuffixOf "suffix!" ua
        return dummyResp
      return ()

testClientCompression :: TestTree
testClientCompression =
  csTest' "client-only compression: no errors" client server
  where
    client =
      TestClient (ClientConfig
                   "localhost"
                   50051
                   [CompressionAlgArg GrpcCompressDeflate]
                   Nothing
                   Nothing) $ \c -> do
        rm <- clientRegisterMethodNormal c "/foo"
        void $ clientRequest c rm 1 "hello" mempty
    server = TestServer (serverConf (["/foo"],[],[],[])) $ \s -> do
      let rm = head (normalMethods s)
      serverHandleNormalCall s rm mempty $ \c -> do
        payload c @?= "hello"
        return dummyResp
      return ()

testClientServerCompression :: TestTree
testClientServerCompression =
  csTest' "client/server compression: no errors" client server
  where
    cconf = ClientConfig "localhost"
                         50051
                         [CompressionAlgArg GrpcCompressDeflate]
                         Nothing
                         Nothing
    client = TestClient cconf $ \c -> do
      rm <- clientRegisterMethodNormal c "/foo"
      clientRequest c rm 1 "hello" mempty >>= do
        checkReqRslt $ \NormalRequestResult{..} -> do
          rspCode @?= StatusOk
          rspBody @?= "hello"
          details @?= ""
          initMD  @?= dummyMeta
          trailMD @?= dummyMeta
      return ()
    sconf = ServerConfig "localhost"
                         50051
                         ["/foo"] [] [] []
                         [CompressionAlgArg GrpcCompressDeflate]
                         Nothing
    server = TestServer sconf $ \s -> do
      let rm = head (normalMethods s)
      serverHandleNormalCall s rm dummyMeta $ \sc -> do
        payload sc @?= "hello"
        return ("hello", dummyMeta, StatusOk, StatusDetails "")
      return ()

testClientServerCompressionLvl :: TestTree
testClientServerCompressionLvl =
  csTest' "client/server compression: no errors" client server
  where
    cconf = ClientConfig "localhost"
                         50051
                         [CompressionLevelArg GrpcCompressLevelHigh]
                         Nothing
                         Nothing
    client = TestClient cconf $ \c -> do
      rm <- clientRegisterMethodNormal c "/foo"
      clientRequest c rm 1 "hello" mempty >>= do
        checkReqRslt $ \NormalRequestResult{..} -> do
          rspCode @?= StatusOk
          rspBody @?= "hello"
          details @?= ""
          initMD  @?= dummyMeta
          trailMD @?= dummyMeta
      return ()
    sconf = ServerConfig "localhost"
                         50051
                         ["/foo"] [] [] []
                         [CompressionLevelArg GrpcCompressLevelLow]
                         Nothing
    server = TestServer sconf $ \s -> do
      let rm = head (normalMethods s)
      serverHandleNormalCall s rm dummyMeta $ \sc -> do
        payload sc @?= "hello"
        return ("hello", dummyMeta, StatusOk, StatusDetails "")
      return ()

testClientMaxReceiveMessageLengthChannelArg :: TestTree
testClientMaxReceiveMessageLengthChannelArg = do
  testGroup "max receive message length channel arg (client channel)"
    [ csTest' "payload size < small bound succeeds" shouldSucceed server
    , csTest' "payload size > small bound fails"    shouldFail    server
    ]
  where
    -- The server always sends a 4-byte payload
    pay    = "four"
    server = TestServer (ServerConfig "localhost" 50051 ["/foo"] [] [] [] [] Nothing) $ \s -> do
      let rm = head (normalMethods s)
      void $ serverHandleNormalCall s rm mempty $ \sc -> do
        payload sc @?= pay
        pure (pay, mempty, StatusOk, StatusDetails "")

    clientMax n k = TestClient conf $ \c -> do
      rm <- clientRegisterMethodNormal c "/foo"
      clientRequest c rm 1 pay mempty >>= k
      where
        conf = ClientConfig "localhost" 50051 [MaxReceiveMessageLength n] Nothing Nothing

    -- Expect success when the max recv payload size is set to 4 bytes, and we
    -- are sent 4.
    shouldSucceed = clientMax 4 $ checkReqRslt $ \NormalRequestResult{..} -> do
      rspCode @?= StatusOk
      rspBody @?= pay
      details @?= ""

    -- Expect failure when the max recv payload size is set to 3 bytes, and we
    -- are sent 4.
    shouldFail = clientMax 3 $ \case
        Left (GRPCIOBadStatusCode StatusResourceExhausted _)
          -> pure ()
        rsp
          -> clientFail ("Expected failure response, but got: " ++ show rsp)

--------------------------------------------------------------------------------
-- Utilities and helpers

is :: (Eq a, Show a, MonadIO m) => m a -> a -> m ()
is act x = act >>= liftIO . (@?= x)

dummyMeta :: MetadataMap
dummyMeta = [("foo","bar")]

dummyResp :: (ByteString, MetadataMap, StatusCode, StatusDetails)
dummyResp = ("", mempty, StatusOk, StatusDetails "")

dummyHandler :: ServerCall a
                -> IO (ByteString, MetadataMap, StatusCode, StatusDetails)
dummyHandler _ = return dummyResp

dummyResult' :: StatusDetails
             -> IO (ByteString, MetadataMap, StatusCode, StatusDetails)
dummyResult' = return . (mempty, mempty, StatusOk, )

nop :: Monad m => a -> m ()
nop = const (return ())

serverOnlyTest :: TestName
               -> ([MethodName],[MethodName],[MethodName],[MethodName])
               -> (Server -> IO ())
               -> TestTree
serverOnlyTest nm ms =
  testCase ("Server - " ++ nm) . runTestServer . TestServer (serverConf ms)

clientOnlyTest :: TestName -> (Client -> IO ()) -> TestTree
clientOnlyTest nm =
  testCase ("Client - " ++ nm) . runTestClient . stdTestClient

csTest :: TestName
       -> (Client -> IO ())
       -> (Server -> IO ())
       -> ([MethodName],[MethodName],[MethodName],[MethodName])
       -> TestTree
csTest nm c s ms =
  csTest' nm (stdTestClient c) (TestServer (serverConf ms) s)

csTest' :: TestName -> TestClient -> TestServer -> TestTree
csTest' nm tc ts =
  testCase ("Client/Server - " ++ nm)
  $ void (s `concurrently` c)
  where
    -- We use a small delay to give the server a head start
    c = threadDelay 100000 >> runTestClient tc
    s = runTestServer ts

-- | @checkMD msg expected actual@ fails when keys from @expected@ are not in
-- @actual@, or when values differ for matching keys.
checkMD :: String -> MetadataMap -> MetadataMap -> Assertion
checkMD desc expected actual =
    assertEqual desc expected' (actual' `S.intersection` expected')
  where
    expected' = fromList . toList $ expected
    actual' = fromList . toList $ actual

checkReqRslt :: Show a => (b -> Assertion) -> Either a b -> Assertion
checkReqRslt = either clientFail

-- | The consumer which asserts that the next value it consumes is equal to the
-- given value; string parameter used as in 'assertEqual'.
assertConsumeEq :: (Eq a, Show a) => String -> a -> P.Consumer a IO ()
assertConsumeEq s v = P.lift . assertEqual s v =<< P.await

clientFail :: Show a => a -> Assertion
clientFail = assertFailure . ("Client error: " ++). show

data TestClient = TestClient ClientConfig (Client -> IO ())

runTestClient :: TestClient -> IO ()
runTestClient (TestClient conf f) =
  runManaged $ mgdGRPC >>= mgdClient conf >>= liftIO . f

stdTestClient :: (Client -> IO ()) -> TestClient
stdTestClient = TestClient stdClientConf

stdClientConf :: ClientConfig
stdClientConf = ClientConfig "localhost" 50051 [] Nothing Nothing

data TestServer = TestServer ServerConfig (Server -> IO ())

runTestServer :: TestServer -> IO ()
runTestServer (TestServer conf f) =
  runManaged $ mgdGRPC >>= mgdServer conf >>= liftIO . f

defServerConf :: ServerConfig
defServerConf = ServerConfig "localhost" 50051 [] [] [] [] [] Nothing

serverConf :: ([MethodName],[MethodName],[MethodName],[MethodName])
              -> ServerConfig
serverConf (ns, cs, ss, bs) =
  defServerConf {methodsToRegisterNormal = ns,
                 methodsToRegisterClientStreaming = cs,
                 methodsToRegisterServerStreaming = ss,
                 methodsToRegisterBiDiStreaming = bs}

mgdGRPC :: Managed GRPC
mgdGRPC = managed withGRPC

mgdClient :: ClientConfig -> GRPC -> Managed Client
mgdClient conf g = managed $ withClient g conf

mgdServer :: ServerConfig -> GRPC -> Managed Server
mgdServer conf g = managed $ withServer g conf