{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE UndecidableInstances #-} module HTTP where import Control.Applicative (Applicative) import Control.Monad.Error (MonadError, catchError, throwError) import Control.Monad.Reader (MonadReader, ReaderT, ask, runReaderT) import Control.Monad.Trans.Class (MonadTrans, lift) import Test.Hspec (Spec, describe, it, shouldBe) import Network.HTTP.Client ( Manager , createCookieJar , defaultManagerSettings , newManager ) import Network.HTTP.Client.Internal (Response(..), ResponseClose(..)) import Network.HTTP.Types.Status (mkStatus) import Network.HTTP.Types.Version (http11) import qualified Control.Monad.Trans.Reader as Reader import qualified Data.Aeson as A import qualified Data.ByteString.Lazy as BL import qualified Data.HashMap.Strict as HM import Network.Pusher.Internal.HTTP ( MonadHTTP(httpLbs) , get , post ) -- Need to use newtype, otherwise there would be overlapping instances with the -- existing ReaderT insatnce of MonadHTTP -- Unfortunately most of boilerplate lines below are require because of this -- newtype instance newtype MockServer m a = MockServer { server :: ReaderT (Response BL.ByteString) m a } deriving (Applicative, Functor, Monad, MonadTrans) runMockServer :: MockServer m a -> Response BL.ByteString -> m a runMockServer (MockServer s) = runReaderT s deriving instance Monad m => MonadReader (Response BL.ByteString) (MockServer m) liftCatchMockerServer :: (m a -> (e -> m a) -> m a) -> MockServer m a -> (e -> MockServer m a) -> MockServer m a liftCatchMockerServer catcher run handler = MockServer $ Reader.liftCatch catcher (server run) (server . handler) instance (MonadError e m) => MonadError e (MockServer m) where throwError = lift . throwError catchError = liftCatchMockerServer catchError instance Monad m => MonadHTTP (MockServer m) where httpLbs _ _ = ask succeededResponse :: Response BL.ByteString succeededResponse = Response { responseStatus = mkStatus 200 "succeess" , responseVersion = http11 , responseHeaders = [] , responseBody = "{\"data\":\"some body\"}" , responseCookieJar = createCookieJar [] , responseClose' = ResponseClose (return () :: IO ()) } failedResponse :: Response BL.ByteString failedResponse = Response { responseStatus = mkStatus 404 "fail" , responseVersion = http11 , responseHeaders = [] , responseBody = "" , responseCookieJar = createCookieJar [] , responseClose' = ResponseClose (return () :: IO ()) } -- |Create a connection manager. This will be ignored by the mock server, but we -- need it in order to type check. withConnManager :: (Manager -> IO a) -> IO a withConnManager run = newManager defaultManagerSettings >>= run test :: Spec test = do describe "HTTP.get" $ do it "returns the body when the request is 200" $ withConnManager $ \mngr -> runMockServer (get mngr "http://example.com/path" []) succeededResponse `shouldBe` (Right $ A.Object $ HM.singleton "data" (A.String "some body")) it "returns an error when the request fails" $ withConnManager $ \mngr -> (runMockServer (get mngr "http://example.com/path" []) failedResponse :: Either String ()) `shouldBe` Left "\"fail\"" describe "HTTP.post" $ do it "returns the body when the request is 200" $ withConnManager $ \mngr -> -- TODO: Need a way of checking the POST data that is sent to the server runMockServer (post mngr "http://example.com/path" [] (A.Object HM.empty)) succeededResponse `shouldBe` Right () it "returns an error when the request fails" $ withConnManager $ \mngr -> (runMockServer (post mngr "http://example.com/path" [] (A.Object HM.empty)) failedResponse :: Either String ()) `shouldBe` Left "\"fail\""