packages feed

http2-5.4.14: test/HTTP2/ClientSpec.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}

module HTTP2.ClientSpec (spec) where

import Control.Concurrent
import qualified Control.Exception as E
import Control.Monad
import Data.ByteString.Builder (byteString)
import Data.Foldable (for_)
import Data.Maybe
import Data.Traversable (for)
import Network.HTTP.Semantics
import Network.HTTP.Types
import Network.Run.TCP hiding (defaultSettings)
import qualified Network.Socket as NS
import qualified Network.Socket.ByteString as NSB
import System.IO.Unsafe (unsafePerformIO)
import System.Random
import System.Timeout (timeout)
import Test.Hspec

import Network.HTTP2.Client
import qualified Network.HTTP2.Server as S

port :: String
port = show $ unsafePerformIO (randomPort <$> getStdGen)
  where
    randomPort = fst . randomR (43124 :: Int, 44320)

host :: String
host = "127.0.0.1"

spec :: Spec
spec = do
    describe "client" $ do
        it "receives an error if scheme is missing" $
            withServer (runServer defaultServer) $ do
                threadDelay 10000
                runClient "" host (defaultClient []) `shouldThrow` streamError

        it "receives an error if authority is missing" $
            withServer (runServer defaultServer) $ do
                threadDelay 10000
                runClient "http" "" (defaultClient []) `shouldThrow` streamError

        it "receives an error if authority and host are different" $
            withServer (runServer defaultServer) $ do
                threadDelay 10000
                runClient "http" host (defaultClient [("Host", "foo")])
                    `shouldThrow` streamError

        it "does not deadlock (in concurrent setting)" $
            withServer (runServer irresponsiveServer) $ do
                threadDelay 10000
                resultVar <- newEmptyMVar
                runClient "http" "localhost" $ concurrentClient resultVar
                result <- timeout 1000000 $ takeMVar resultVar
                case result of
                    Nothing -> expectationFailure "Exception was not raised"
                    Just (Left ConnectionIsClosed) -> return ()
                    Just (Left err) -> expectationFailure $ "Raise unexpected exception " ++ show err
                    Just (Right ()) -> expectationFailure "Unexpected client termination"

        it "reports a connection the server resets as ConnectionIsReset" $
            withResettingServer $ \p -> do
                r <- E.try $ runClientTo p (defaultClient [])
                case r of
                    Left ConnectionIsReset -> return ()
                    _ -> expectationFailure $ "Unexpected result: " ++ show r

        it "respects max concurrent streams setting" $
            withServer (runServer irresponsiveServer) $ do
                threadDelay 10000
                let maxConc = fromJust $ maxConcurrentStreams defaultSettings

                resultVars <- runClient "http" "localhost" $ \sendReq aux -> do
                    replicateM ((maxConc + 1) :: Int) $ do
                        resultVar <- newEmptyMVar
                        concurrentClient resultVar sendReq aux
                        pure resultVar

                let acceptedRequestVars = take maxConc resultVars
                acceptedResults <- for acceptedRequestVars (timeout 1000000 . takeMVar)
                for_ acceptedResults $ \case
                    Nothing -> expectationFailure "Exception was not raised"
                    Just (Left ConnectionIsClosed) -> return ()
                    Just (Left err) -> expectationFailure $ "Raise unexpected exception " ++ show err
                    Just (Right ()) -> expectationFailure "Unexpected client termination"

                let waitingRequestVars = drop maxConc resultVars
                waitingResults <- for waitingRequestVars (timeout 1000000 . takeMVar)
                for_ waitingResults $ \case
                    Nothing -> pure ()
                    Just (Left err) -> expectationFailure $ "Raise unexpected exception " ++ show err
                    Just (Right ()) -> expectationFailure "Unexpected client termination"

runServer :: S.Server -> ServerSettings -> IO ()
runServer server set = runTCPServerWithSettings set (Just host) port runHTTP2Server
  where
    runHTTP2Server s =
        E.bracket
            (allocSimpleConfig s 4096)
            freeSimpleConfig
            (\conf -> S.run S.defaultServerConfig conf server)

defaultServer :: S.Server
defaultServer _req _aux sendResponse = sendResponse responseHello []

irresponsiveServer :: S.Server
irresponsiveServer _req _aux _sendResponse = do
    forever $ threadDelay 10000000

responseHello :: S.Response
responseHello = S.responseBuilder ok200 header body
  where
    header = [("Content-Type", "text/plain")]
    body = byteString "Hello, world!\n"

runClient :: Scheme -> Authority -> Client a -> IO a
runClient = runClientWith port

runClientTo :: String -> Client a -> IO a
runClientTo p = runClientWith p "http" "localhost"

runClientWith :: String -> Scheme -> Authority -> Client a -> IO a
runClientWith p sc au client = runTCPClient host p runHTTP2Client
  where
    cliconf = defaultClientConfig{scheme = sc, authority = au}
    runHTTP2Client s =
        E.bracket
            (allocSimpleConfig s 4096)
            freeSimpleConfig
            ( \conf -> run cliconf conf $ \sendRequest ->
                client sendRequest
            )

defaultClient :: RequestHeaders -> Client ()
defaultClient hd sendRequest _aux = do
    let req = requestNoBody methodGet "/" hd
    sendRequest req $ \rsp -> do
        responseStatus rsp `shouldBe` Just ok200
        fmap statusMessage (responseStatus rsp) `shouldBe` Just "OK"

concurrentClient :: MVar (Either HTTP2Error ()) -> Client ()
concurrentClient resultVar sendRequest _aux = do
    let req = requestNoBody methodGet "/" []
    void $ forkIO $ do
        result <- E.try $ sendRequest req $ \_rsp -> return ()
        putMVar resultVar result
    threadDelay 10000

-- | A malformed request is a stream error (RFC 9113 section 8.1.1), so the
-- server resets that stream and the connection carries on.  The client learns
-- of it through the stream it was waiting on, as 'StreamResetIsReceived'.
--
-- This used to also admit 'ConnectionErrorIsReceived', from back when the
-- server escalated every stream error to the connection and answered one bad
-- request by hanging up on all of them.
streamError :: Selector HTTP2Error
streamError StreamResetIsReceived{} = True
streamError _ = False

-- | A server which answers the client's SETTINGS and then resets the
--   connection, as one killed in the middle of a request does.
withResettingServer :: (String -> IO a) -> IO a
withResettingServer action =
    E.bracket listenLocal NS.close $ \ls -> do
        p <- NS.socketPort ls
        _ <- forkIO $ do
            (s, _) <- NS.accept ls
            _ <- NSB.recv s 4096
            NSB.sendAll s "\0\0\0\4\0\0\0\0\0" -- an empty SETTINGS
            threadDelay 100000
            NS.setSockOpt s NS.Linger (NS.StructLinger 1 0)
            NS.close s
        action $ show p
  where
    listenLocal = do
        ls <- NS.socket NS.AF_INET NS.Stream NS.defaultProtocol
        NS.bind ls $ NS.SockAddrInet 0 $ NS.tupleToHostAddress (127, 0, 0, 1)
        NS.listen ls 1
        return ls

-- | Running a test server for the duration of the action, then telling it
--   to stop.  'killThread' is no good here: it does not reach a thread
--   blocked in @accept@ on Windows, where the call is a safe foreign one,
--   so the whole suite used to hang on the first of these.
withServer :: (ServerSettings -> IO ()) -> IO a -> IO a
withServer runIt action = do
    stopVar <- newEmptyMVar
    done <- newEmptyMVar
    let set = defaultServerSettings{settingsInstallShutdownHandler = putMVar stopVar}
    _ <- forkIO $ runIt set `E.finally` putMVar done ()
    action `E.finally` do
        join $ takeMVar stopVar
        takeMVar done