packages feed

dormouse-client 0.1.0.1 → 0.2.0.0

raw patch · 5 files changed

+44/−264 lines, 5 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Dormouse.Client.Test.Class: expectBs :: MonadDormouseTestClient m => HttpRequest scheme method ByteString contentTag acceptTag -> m (HttpResponse ByteString)
+ Dormouse.Client.Test.Class: expectBs :: (MonadDormouseTestClient m, IsUrl url) => HttpRequest url method ByteString contentTag acceptTag -> m (HttpResponse ByteString)
- Dormouse.Client.Test.Class: expectLbs :: MonadDormouseTestClient m => HttpRequest scheme method ByteString contentTag acceptTag -> m (HttpResponse ByteString)
+ Dormouse.Client.Test.Class: expectLbs :: (MonadDormouseTestClient m, IsUrl url) => HttpRequest url method ByteString contentTag acceptTag -> m (HttpResponse ByteString)

Files

ChangeLog.md view
@@ -1,3 +1,7 @@-# Changelog for dormouse+# Changelog for dormouse-client++## v0.2.0.0++- Added `IsUrl url` constraint to functions in `MonadDormouseTestClient` so that `asAnyUrl` can be applied to the supplied urls.  This facilitates testing correct urls are supplied.  ## Unreleased changes
dormouse-client.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 53a8941fce30957b33c0c2573f20f976e663b6a0d21e6feed28a68ff8b5d68e3+-- hash: 196468c6e841b939083b6f97fca2642a4a3be8d5b8e7cf475711a583af3f470a  name:           dormouse-client-version:        0.1.0.1+version:        0.2.0.0 synopsis:       Simple, type-safe and testable HTTP client description:    An HTTP client designed to be productive, easy to use, easy to test, flexible and safe!                 .@@ -97,7 +97,6 @@       Dormouse.Client.Types       Dormouse.Client.Generators.Json       Dormouse.Client.Generators.Text-      Dormouse.Client.Generators.UriComponents       Dormouse.Client.Headers.MediaTypeSpec       Dormouse.Client.PayloadSpec       Dormouse.Client.StatusSpec
src/Dormouse/Client/Test/Class.hs view
@@ -25,6 +25,7 @@ import Dormouse.Client.Class ( MonadDormouseClient(..) ) import Dormouse.Client.Payload ( RawRequestPayload(..) ) import Dormouse.Client.Types ( HttpRequest(..), HttpResponse(..) )+import Dormouse.Url ( IsUrl ) import Streamly ( SerialT ) import qualified Streamly.Prelude as S import qualified Streamly.External.ByteString as SEB@@ -33,12 +34,12 @@ -- | MonadDormouseTestClient describes the capability to send and receive specifically ByteString typed HTTP Requests and Responses class Monad m => MonadDormouseTestClient m where   -- | Make the supplied HTTP request, expecting an HTTP response with a Lazy ByteString body to be delivered in some @MonadDormouseTest m@-  expectLbs :: HttpRequest scheme method LB.ByteString contentTag acceptTag -> m (HttpResponse LB.ByteString)+  expectLbs :: IsUrl url => HttpRequest url method LB.ByteString contentTag acceptTag -> m (HttpResponse LB.ByteString)   expectLbs req = do     resp <- expectBs $ req {requestBody = LB.toStrict $ requestBody req}     return $ resp {responseBody = LB.fromStrict $ responseBody resp}   -- | Make the supplied HTTP request, expecting an HTTP response with a Strict ByteString body to be delivered in some @MonadDormouseTest m@-  expectBs :: HttpRequest scheme method SB.ByteString contentTag acceptTag -> m (HttpResponse SB.ByteString)+  expectBs :: IsUrl url => HttpRequest url method SB.ByteString contentTag acceptTag -> m (HttpResponse SB.ByteString)   expectBs req = do     resp <- expectLbs $ req {requestBody = LB.fromStrict $ requestBody req}     return $ resp {responseBody = LB.toStrict $ responseBody resp}
− test/Dormouse/Client/Generators/UriComponents.hs
@@ -1,246 +0,0 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}--module Dormouse.Client.Generators.UriComponents -  ( genValidScheme-  , genInvalidScheme-  , genValidUsername-  , genInvalidUsername-  , genValidPassword-  , genInvalidPassword-  , genValidUserInfo-  , genInvalidUserInfo-  , genValidIPv4-  , genValidRegName-  , genValidHost-  , genValidPort-  , genValidAuthority-  , genValidPathAbsAuth-  , genValidPathAbsNoAuth-  , genValidPathRel-  , genValidQuery-  , genValidFragment-  , genValidAbsoluteUri-  )-  where--import qualified Data.ByteString as B-import qualified Data.ByteString.Char8 as B8-import Data.ByteString.Internal (c2w, w2c)-import qualified Data.Char as C-import qualified Data.Text as T-import Dormouse.Uri.Encode-import Dormouse.Uri.RFC3986-import Hedgehog-import qualified Hedgehog.Gen as Gen-import qualified Hedgehog.Range as Range--genPercentEncoded :: Gen B.ByteString-genPercentEncoded = do-  char <- Gen.filter C.isPrint Gen.unicode-  let t = T.pack $ [char]-  let percentEncoded = encodeUnless (const False) t-  return $ percentEncoded--genValidScheme :: Gen B.ByteString-genValidScheme = do-  first <- Gen.filter isAsciiAlpha Gen.ascii-  bs <- Gen.list (Range.constant 0 10) (Gen.filter isSchemeChar Gen.ascii)-  return . B8.pack $ (first : bs ++ [':'])--data SchemeFailureMode -  = NonAsciiFirstChar-  | InvalidSchemeChar-  | NoTrailingSemicolon--schemeFailureMode :: Int -> SchemeFailureMode-schemeFailureMode 1 = NonAsciiFirstChar-schemeFailureMode 2 = InvalidSchemeChar-schemeFailureMode 3 = NoTrailingSemicolon-schemeFailureMode _ = undefined--genInvalidScheme :: Gen B.ByteString-genInvalidScheme = do-  failureMode <- fmap schemeFailureMode $ Gen.element [1..3]-  first <- case failureMode of-    NonAsciiFirstChar -> Gen.filter (not . isAsciiAlpha) Gen.ascii-    _                 -> Gen.filter isAsciiAlpha Gen.ascii-  remainder <- case failureMode of-    InvalidSchemeChar -> do-      let c = Gen.filter (\x -> (not $ isSchemeChar x) && x /= ':') Gen.ascii-      Gen.list (Range.constant 1 10) c-    _ -> do-      let c = Gen.filter isSchemeChar Gen.ascii-      Gen.list (Range.constant 0 10) c-  let finalBs = case failureMode of-        NoTrailingSemicolon -> first : remainder-        _                   -> first : remainder ++ [':']-  return $ B8.pack finalBs--genUsernameChar :: Gen B.ByteString-genUsernameChar = Gen.frequency [(1, genPercentEncoded), (25, fmap (B8.pack . return) $ Gen.filter isUsernameChar Gen.ascii)]--genValidUsername :: Gen B.ByteString-genValidUsername = do-  list <- Gen.list (Range.constant 1 20) genUsernameChar-  return $ B.intercalate "" list--genInvalidUsername :: Gen B.ByteString-genInvalidUsername = do-  invalids <- Gen.list (Range.constant 1 5) genInvalidUsernameChar-  valids <- Gen.list (Range.constant 0 15) genUsernameChar-  fmap (B.intercalate "") $ Gen.shuffle $ invalids ++ valids-  where-    genInvalidUsernameChar = fmap (B8.pack . return) $ Gen.filter (\x -> (not $ isUsernameChar x) && x /= '%'  && C.isPrint x) Gen.ascii--genPasswordChar :: Gen B.ByteString-genPasswordChar = Gen.frequency [(1, genPercentEncoded), (25, fmap (B8.pack . return) $ Gen.filter isPasswordChar Gen.ascii)]--genValidPassword :: Gen B.ByteString-genValidPassword = do-  list <- Gen.list (Range.constant 1 20) genPasswordChar-  return $ B.intercalate "" list--genInvalidPassword :: Gen B.ByteString-genInvalidPassword = do-  invalids <- Gen.list (Range.constant 1 5) genInvalidPasswordChar-  valids <- Gen.list (Range.constant 0 15) genPasswordChar-  fmap (B.intercalate "") $ Gen.shuffle $ invalids ++ valids-  where-    genInvalidPasswordChar = fmap (B8.pack . return) $ Gen.filter (\x -> (not $ isPasswordChar x) && x /= '%' && C.isPrint x) Gen.ascii--genValidUserInfo :: Gen B.ByteString-genValidUserInfo = do-  username <- genValidUsername-  maybePassword <- Gen.maybe genValidPassword-  let passwordSuffix = maybe B.empty (B.cons $ c2w ':') maybePassword-  return $ B.append (B.append username passwordSuffix) "@"--data UserInfoFailureMode -  = InvalidUsername-  | InvalidPassword-  | MissingAtSuffix--userInfoFailureMode :: Int -> UserInfoFailureMode-userInfoFailureMode 1 = InvalidUsername-userInfoFailureMode 2 = InvalidPassword-userInfoFailureMode 3 = MissingAtSuffix-userInfoFailureMode _ = undefined--genInvalidUserInfo :: Gen B.ByteString-genInvalidUserInfo = do-  failureMode <- fmap userInfoFailureMode $ Gen.element [1..3]-  username <- case failureMode of-    InvalidUsername -> Gen.filter (B.all (\x -> w2c x /= ':')) genInvalidUsername -- if the username is supposed to be invalid, ensure that ':' is not present, otherwise the user info could be interpreted as valid if valid chars precede the ':'-    _               -> genValidUsername-  maybePassword <- case failureMode of -    InvalidPassword -> fmap Just genInvalidPassword-    _               -> Gen.maybe $ genValidPassword-  let passwordSuffix = maybe B.empty (B.cons $ c2w ':') maybePassword-  let complete = case failureMode of-        MissingAtSuffix -> B.append (B.append username passwordSuffix) "#"-        _               -> B.append (B.append username passwordSuffix) "@"-  return complete--genValidIPv4 :: Gen B.ByteString-genValidIPv4 = do-  ipChars <- (\a b c d -> show a <> "." <> show b <> "." <> show c <> "." <> show d) <$> genOctet <*> genOctet <*> genOctet <*> genOctet-  return $ B8.pack ipChars-  where-    genOctet = Gen.word8 Range.constantBounded--genValidRegName :: Gen B.ByteString-genValidRegName = Gen.frequency [(1, genPercentEncoded), (25, fmap (B8.pack . return) $ Gen.filter isRegNameChar Gen.ascii)]--genValidHost :: Gen B.ByteString-genValidHost = Gen.choice [genValidIPv4, genValidRegName]--genValidPort :: Gen B.ByteString-genValidPort = fmap (B.append ":" . B8.pack . show) $ Gen.word16 Range.constantBounded--genValidAuthority :: Gen B.ByteString-genValidAuthority = do-  maybeUserInfo <- Gen.maybe genValidUserInfo-  let userInfoPrefix = maybe B.empty id maybeUserInfo-  host <- genValidHost-  maybePort <- Gen.maybe genValidPort-  let portSuffix = maybe B.empty id maybePort-  return . B.append "//" . B.append userInfoPrefix $ B.append host portSuffix--genPathChar :: Gen B.ByteString-genPathChar = Gen.frequency [(1, genPercentEncoded), (25, fmap (B8.pack . return) $ Gen.filter isPathChar Gen.ascii)]--genPathSegment :: Gen B.ByteString-genPathSegment = fmap (B.intercalate "") $ Gen.list (Range.constant 0 20) genPathChar--genPathSegmentNz :: Gen B.ByteString-genPathSegmentNz = fmap (B.intercalate "") $ Gen.list (Range.constant 1 20) genPathChar--genPathCharNc :: Gen B.ByteString-genPathCharNc = Gen.frequency [(1, genPercentEncoded), (25, fmap (B8.pack . return) $ Gen.filter isPathCharNoColon Gen.ascii)]--genPathSegmentNzNc :: Gen B.ByteString-genPathSegmentNzNc = fmap (B.intercalate "") $ Gen.list (Range.constant 1 20) genPathCharNc--genPathsAbEmpty :: Gen B.ByteString-genPathsAbEmpty = do-  components <- Gen.list (Range.constant 1 10) genPathSegment-  return . B.append "/" $ B.intercalate "/" components--genPathsAbsolute :: Gen B.ByteString-genPathsAbsolute = do-  first <- genPathSegmentNz-  components <- Gen.list (Range.constant 0 10) genPathSegment-  return . B.append "/" . B.append first $ B.intercalate "/" components--genPathsNoScheme :: Gen B.ByteString-genPathsNoScheme = do-  first <- genPathSegmentNzNc-  components <- Gen.list (Range.constant 0 10) genPathSegment-  return . B.append first . B.append "/" $ B.intercalate "/" components--genPathsRootless :: Gen B.ByteString-genPathsRootless = do-  first <- genPathSegmentNz-  components <- Gen.list (Range.constant 0 10) genPathSegment-  return . B.append first . B.append "/" $ B.intercalate "/" components--genValidPathsEmpty :: Gen B.ByteString-genValidPathsEmpty = return B.empty--genValidPathAbsAuth :: Gen B.ByteString-genValidPathAbsAuth = Gen.choice [genPathsAbEmpty, genPathsAbsolute, genValidPathsEmpty]--genValidPathAbsNoAuth :: Gen B.ByteString-genValidPathAbsNoAuth = Gen.choice [genPathsAbsolute, genPathsRootless, genValidPathsEmpty]--genValidPathRel :: Gen B.ByteString-genValidPathRel = Gen.choice [genPathsAbsolute, genPathsNoScheme, genValidPathsEmpty]--genQueryChar :: Gen B.ByteString-genQueryChar = Gen.frequency [(1, genPercentEncoded), (25, fmap (B8.pack . return) $ Gen.filter isQueryChar Gen.ascii)]--genValidQuery :: Gen B.ByteString-genValidQuery = do-  list <- Gen.list (Range.constant 1 50) genQueryChar-  return $ B.append "?" $ B.intercalate "" list--genFragmentChar :: Gen B.ByteString-genFragmentChar = Gen.frequency [(1, genPercentEncoded), (25, fmap (B8.pack . return) $ Gen.filter isFragmentChar Gen.ascii)]--genValidFragment :: Gen B.ByteString-genValidFragment = do-  list <- Gen.list (Range.constant 1 50) genFragmentChar-  return $ B.append "#" $ B.intercalate "" list--genValidAbsoluteUri :: Gen B.ByteString-genValidAbsoluteUri = do-  scheme <- genValidScheme-  authority <- Gen.maybe genValidAuthority-  path <- case authority of-    Just _  -> genValidPathAbsAuth-    Nothing -> genValidPathAbsNoAuth-  query <- Gen.maybe genValidQuery-  fragment <- Gen.maybe genValidFragment-  return . B.intercalate "" $ [scheme, maybe B.empty id authority, path, maybe B.empty id query, maybe B.empty id fragment]--
test/Dormouse/ClientSpec.hs view
@@ -1,7 +1,9 @@ {-# LANGUAGE DataKinds #-}+{-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE ViewPatterns #-}  module Dormouse.ClientSpec    ( spec@@ -29,6 +31,7 @@   , sentContentType :: MVar (Maybe B.ByteString)   , sentAcceptHeader :: MVar (Maybe B.ByteString)   , returnJson :: MVar (Maybe (LB.ByteString))+  , triggered :: MVar Bool   }  newtype TestM a = TestM { unTestM :: ReaderT TestEnv IO a } deriving (Functor, Applicative, Monad, MonadReader TestEnv, MonadIO, MonadThrow)@@ -37,18 +40,29 @@ runTestM deps app = flip runReaderT deps $ unTestM app  instance MonadDormouseTestClient TestM where-  expectLbs req = do-    testEnv <- ask-    _ <- liftIO . swapMVar (sentJson testEnv) . Just $ requestBody req-    _ <- liftIO . swapMVar (sentContentType testEnv) . getHeaderValue "Content-Type" $ req-    _ <- liftIO . swapMVar (sentAcceptHeader testEnv) . getHeaderValue "Accept" $ req-    maybeResponseJson <- liftIO . readMVar $ returnJson testEnv-    let respBs = maybe (encode ()) id $ maybeResponseJson-    pure HttpResponse-      { responseStatusCode = 200-      , responseHeaders = Map.empty-      , responseBody = respBs-      }+  expectLbs (req @ HttpRequest { requestUrl = u, requestMethod = method, requestBody = body, requestHeaders = headers }) = do+    let reqUrl = asAnyUrl u+    case (reqUrl, method) of+      ([url|https://starfleet.com/captains|], GET) -> do+        testEnv <- ask+        _ <- liftIO . swapMVar (triggered testEnv) $ True+        pure HttpResponse+          { responseStatusCode = 200+          , responseHeaders = Map.empty+          , responseBody = encode ()+          }+      _ -> do+        testEnv <- ask+        _ <- liftIO . swapMVar (sentJson testEnv) . Just $ requestBody req+        _ <- liftIO . swapMVar (sentContentType testEnv) . getHeaderValue "Content-Type" $ req+        _ <- liftIO . swapMVar (sentAcceptHeader testEnv) . getHeaderValue "Accept" $ req+        maybeResponseJson <- liftIO . readMVar $ returnJson testEnv+        let respBs = maybe (encode ()) id $ maybeResponseJson+        pure HttpResponse+          { responseStatusCode = 200+          , responseHeaders = Map.empty+          , responseBody = respBs+          }  spec :: Spec spec = before setup $ do@@ -98,17 +112,25 @@         let actualJson = responseBody r         actualJson === arbJson         actualAcceptHeader === Nothing+    it "matches the expected url and method" $ \(_, _, _, _, testEnv, _) -> do+      hedgehog $ do+        let req = get [https|https://starfleet.com/captains|]+        r :: HttpResponse Value <- liftIO $ runTestM testEnv $ expectAs json req+        t <- liftIO . readMVar $ triggered testEnv+        t === True   where      setup = do       sentJson' <- newMVar Nothing       sentContentType' <- newMVar Nothing       sentAcceptHeader' <- newMVar Nothing       returnJson' <- newMVar Nothing+      triggered' <- newMVar False       let testEnv = TestEnv              { sentJson = sentJson'             , sentContentType = sentContentType'             , sentAcceptHeader = sentAcceptHeader'             , returnJson = returnJson'+            , triggered = triggered'             }       let jsonGenRanges = JsonGenRanges              { stringRanges = Range.constant 0 15