packages feed

warp-3.4.16: test/ConnectionExceptionSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module ConnectionExceptionSpec (spec) where

import Control.Concurrent (Chan, newChan, readChan, writeChan, newEmptyMVar, putMVar, takeMVar, tryPutMVar)
import Control.Concurrent.Async (link, withAsync)
import Control.Exception (Exception, SomeException, bracket, finally, fromException, throwIO, toException)
import Control.Monad (forM_, replicateM, void)
import Data.IORef (newIORef, readIORef, writeIORef, modifyIORef')
import Data.Maybe (isJust)
import qualified Data.Streaming.Network as N
import Network.HTTP.Types (internalServerError500)
import Network.Socket (SockAddr (SockAddrInet), close, tupleToHostAddress)
import Network.Wai (remoteHost)
import Network.Wai.Handler.Warp
import Network.Wai.Handler.Warp.Internal (runSettingsConnectionMakerSecure)
import System.Timeout (timeout)
import Test.Hspec

import HTTP (responseStatus, sendGET)

-- Regression for https://github.com/yesodweb/wai/issues/1113. A connection
-- maker can fail before any Request exists, but Warp already owns its peer.
data ConnectionFailure = ConnectionFailure Int deriving (Show)
instance Exception ConnectionFailure

type Observation = (Int, Maybe SockAddr)

spec :: Spec
spec = describe "connection exception peer" $ do
    it "preserves the legacy observer when no peer observer is installed" $ do
        events <- newChan
        makers <- newChan
        let settings = setOnException (\request -> record events (remoteHost <$> request)) defaultSettings
        withAsync (runSettingsConnectionMakerSecure settings (readChan makers) unusedApplication) $ \server -> do
            link server
            writeChan makers (throwIO (ConnectionFailure 1), peer 100)
            timeout 2000000 (readChan events) `shouldReturn` Just (1, Nothing)

    it "gets the current legacy observer as the default connection observer" $ do
        events <- newChan
        let settings = setOnException (\request -> record events (remoteHost <$> request)) defaultSettings
        getOnConnectionException settings (peer 110) (toException (ConnectionFailure 2))
        timeout 2000000 (readChan events) `shouldReturn` Just (2, Nothing)

    it "uses only the connection observer regardless of setter order" $
        forM_ [False, True] $ \legacyLast -> do
            calls <- newIORef ([] :: [String])
            let legacy _ _ = modifyIORef' calls (++ ["legacy"])
                connection _ _ = modifyIORef' calls (++ ["connection"])
                settings = if legacyLast
                    then setOnException legacy $ setOnConnectionException connection defaultSettings
                    else setOnConnectionException connection $ setOnException legacy defaultSettings
            getOnConnectionException settings (peer 120) (toException (ConnectionFailure 3))
            readIORef calls `shouldReturn` ["connection"]

    it "keeps accept failures on the legacy observer because no peer was obtained" $ do
        calls <- newIORef ([] :: [(String, Bool)])
        let legacy request _ = modifyIORef' calls (++ [("legacy", isJust request)])
            connection _ _ = modifyIORef' calls (++ [("connection", False)])
            settings = setOnException legacy $ setOnConnectionException connection defaultSettings
        runSettingsConnectionMakerSecure settings (ioError (userError "accept failed")) unusedApplication
        readIORef calls `shouldReturn` [("legacy", False)]

    it "keeps application exceptions on the legacy observer with their request" $
        bracket (N.bindRandomPortTCP "127.0.0.1") (close . snd) $ \(port, listener) -> do
            events <- newChan
            connectionCalled <- newIORef False
            ready <- newEmptyMVar
            let legacy request exception = writeChan events (isJust request, show exception)
                connection _ _ = writeIORef connectionCalled True
                settings = setBeforeMainLoop (putMVar ready ())
                    $ setOnException legacy
                    $ setOnConnectionException connection defaultSettings
                application _ _ = throwIO (ConnectionFailure 4)
            withAsync (runSettingsSocket settings listener application) $ \server -> do
                link server
                timeout 2000000 (takeMVar ready) `shouldReturn` Just ()
                response <- sendGET ("http://127.0.0.1:" ++ show port ++ "/")
                responseStatus response `shouldBe` internalServerError500
                timeout 2000000 (readChan events) `shouldReturn` Just (True, "ConnectionFailure 4")
                readIORef connectionCalled `shouldReturn` False

    it "reports each sequential connection maker's own peer" $ do
        events <- newChan
        makers <- newChan
        let settings = observePeer (record events) defaultSettings
        withAsync (runSettingsConnectionMakerSecure settings (readChan makers) unusedApplication) $ \server -> do
            link server
            forM_ [(5, 130), (6, 140)] $ \(failureId, peerId) -> do
                writeChan makers (throwIO (ConnectionFailure failureId), peer peerId)
                timeout 2000000 (readChan events) `shouldReturn` Just (failureId, Just (peer peerId))

    it "reports peers when overlapping connection makers fail in reverse order" $ do
        events <- newChan
        makers <- newChan
        started <- newChan
        first <- newEmptyMVar
        second <- newEmptyMVar
        let settings = observePeer (record events) defaultSettings
            maker i gate = writeChan started i >> takeMVar gate >> throwIO (ConnectionFailure i)
        withAsync (runSettingsConnectionMakerSecure settings (readChan makers) unusedApplication) $ \server -> do
            link server
            writeChan makers (maker 7 first, peer 150)
            writeChan makers (maker 8 second, peer 160)
            -- Release both workers even when a readiness assertion fails.
            let release = forM_ [first, second] $ \gate -> void (tryPutMVar gate ())
            flip finally release $ do
                ready <- timeout 2000000 (replicateM 2 (readChan started))
                fmap length ready `shouldBe` Just 2
                putMVar second ()
                secondEvent <- timeout 2000000 (readChan events)
                putMVar first ()
                firstEvent <- timeout 2000000 (readChan events)
                (secondEvent, firstEvent) `shouldBe` (Just (8, Just (peer 160)), Just (7, Just (peer 150)))
  where
    unusedApplication _ _ = fail "connection maker must fail before the application"

-- Install the peer observer used by the regression assertions. The original
-- failing commit had to infer peers from Maybe Request.
observePeer :: (Maybe SockAddr -> SomeException -> IO ()) -> Settings -> Settings
observePeer report = setOnConnectionException (report . Just)

record :: Chan Observation -> Maybe SockAddr -> SomeException -> IO ()
record events address exception = case fromException exception of
    Just (ConnectionFailure i) -> writeChan events (i, address)
    Nothing -> throwIO exception

peer :: Int -> SockAddr
peer i = SockAddrInet (fromIntegral (40000 + i)) (tupleToHostAddress (127, 0, 0, fromIntegral i))