Comprehensive instances for HasGenRequest.

This commit is contained in:
Julian K. Arni 2016-04-23 21:43:03 +02:00
parent 1c1c3dc9bd
commit 3189902c4b
5 changed files with 80 additions and 12 deletions

View File

@ -41,6 +41,7 @@ library
, warp >= 3.2.4 && < 3.3 , warp >= 3.2.4 && < 3.3
, process == 1.2.* , process == 1.2.*
, temporary == 1.2.* , temporary == 1.2.*
, case-insensitive
, hspec , hspec
, text == 1.* , text == 1.*
hs-source-dirs: src hs-source-dirs: src
@ -74,6 +75,7 @@ test-suite spec
, warp , warp
, servant-server , servant-server
, servant-client , servant-client
, servant
, transformers , transformers
, QuickCheck , QuickCheck
, quickcheck-io , quickcheck-io

View File

@ -36,6 +36,7 @@ module Servant.QuickCheck
, (<%>) , (<%>)
, Predicates , Predicates
, not500 , not500
, onlyJsonObjects
-- ** Re-exports -- ** Re-exports
, BaseUrl(..) , BaseUrl(..)

View File

@ -82,6 +82,31 @@ instance (KnownSymbol x, Arbitrary c, ToHttpApiData c, HasGenRequest b)
param = cs $ symbolVal (Proxy :: Proxy x) param = cs $ symbolVal (Proxy :: Proxy x)
new = arbitrary :: Gen c new = arbitrary :: Gen c
instance (KnownSymbol x, Arbitrary c, ToHttpApiData c, HasGenRequest b)
=> HasGenRequest (QueryParams x c :> b) where
genRequest _ = do
new' <- new
old' <- old
return $ \burl -> let r = old' burl in r {
queryString = queryString r
<> if length new' > 0 then fold (toParam <$> new') else ""}
where
old = genRequest (Proxy :: Proxy b)
param = cs $ symbolVal (Proxy :: Proxy x)
new = arbitrary :: Gen [c]
toParam c = param <> "[]=" <> cs (toQueryParam c)
fold = foldr1 (\a b -> a <> "&" <> b)
instance (KnownSymbol x, HasGenRequest b)
=> HasGenRequest (QueryFlag x :> b) where
genRequest _ = do
old' <- old
return $ \burl -> let r = old' burl in r {
queryString = queryString r <> param <> "=" }
where
old = genRequest (Proxy :: Proxy b)
param = cs $ symbolVal (Proxy :: Proxy x)
instance (ReflectMethod method) instance (ReflectMethod method)
=> HasGenRequest (Verb (method :: k) (status :: Nat) (cts :: [*]) a) where => HasGenRequest (Verb (method :: k) (status :: Nat) (cts :: [*]) a) where
genRequest _ = return $ \burl -> def genRequest _ = return $ \burl -> def
@ -91,3 +116,17 @@ instance (ReflectMethod method)
, method = reflectMethod (Proxy :: Proxy method) , method = reflectMethod (Proxy :: Proxy method)
} }
instance (HasGenRequest a) => HasGenRequest (RemoteHost :> a) where
genRequest _ = genRequest (Proxy :: Proxy a)
instance (HasGenRequest a) => HasGenRequest (IsSecure :> a) where
genRequest _ = genRequest (Proxy :: Proxy a)
instance (HasGenRequest a) => HasGenRequest (HttpVersion :> a) where
genRequest _ = genRequest (Proxy :: Proxy a)
instance (HasGenRequest a) => HasGenRequest (Vault :> a) where
genRequest _ = genRequest (Proxy :: Proxy a)
instance (HasGenRequest a) => HasGenRequest (WithNamedContext x y a) where
genRequest _ = genRequest (Proxy :: Proxy a)

View File

@ -1,13 +1,17 @@
module Servant.QuickCheck.Internal.Predicates where module Servant.QuickCheck.Internal.Predicates where
import Data.Monoid ((<>)) import Control.Monad
import GHC.Generics (Generic) import Data.Aeson (Object, decode)
import Control.Monad import Data.Bifunctor (Bifunctor (..))
import Network.HTTP.Client (Request, Response, responseStatus, Manager, httpLbs)
import Network.HTTP.Types (status500)
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import Data.Bifunctor (Bifunctor(..)) import qualified Data.ByteString as SBS
import Data.Text (Text) import Data.CaseInsensitive (mk)
import Data.Monoid ((<>))
import Data.Text (Text)
import GHC.Generics (Generic)
import Network.HTTP.Client (Manager, Request, Response, httpLbs,
responseBody, responseStatus, responseHeaders)
import Network.HTTP.Types (status500)
-- | @500 Internal Server Error@ should be avoided - it may represent some -- | @500 Internal Server Error@ should be avoided - it may represent some
-- issue with the application code, and it moreover gives the client little -- issue with the application code, and it moreover gives the client little
@ -17,7 +21,6 @@ import Data.Text (Text)
not500 :: ResponsePredicate Text Bool not500 :: ResponsePredicate Text Bool
not500 = ResponsePredicate "not500" (\resp -> not $ responseStatus resp == status500) not500 = ResponsePredicate "not500" (\resp -> not $ responseStatus resp == status500)
{-
-- | Returning anything other than an object when returning JSON is considered -- | Returning anything other than an object when returning JSON is considered
-- bad practice, as: -- bad practice, as:
-- --
@ -30,10 +33,13 @@ not500 = ResponsePredicate "not500" (\resp -> not $ responseStatus resp == statu
-- --
-- This function checks that any @application/json@ responses only return JSON -- This function checks that any @application/json@ responses only return JSON
-- objects (and not arrays, strings, numbers, or booleans) at the top level. -- objects (and not arrays, strings, numbers, or booleans) at the top level.
onlyJsonObjects :: Response b -> IO Bool onlyJsonObjects :: ResponsePredicate Text Bool
onlyJsonObjects onlyJsonObjects
= ResponsePredicate "onlyJsonObjects" _ = ResponsePredicate "onlyJsonObjects" (\resp -> case decode (responseBody resp) of
Nothing -> False
Just (_ :: Object) -> True)
{-
-- | When creating a new resource, it is good practice to provide a @Location@ -- | When creating a new resource, it is good practice to provide a @Location@
-- header with a link to the created resource. -- header with a link to the created resource.
-- --
@ -42,9 +48,9 @@ onlyJsonObjects
-- requests. -- requests.
-- --
-- References: <RFC 7231, Section 6.3.2 https://tools.ietf.org/html/rfc7231#section-6.3.2> -- References: <RFC 7231, Section 6.3.2 https://tools.ietf.org/html/rfc7231#section-6.3.2>
createContainsValidLocation :: Response b -> IO Bool createContainsValidLocation :: ResponsePredicate Text Bool
createContainsValidLocation createContainsValidLocation
= ResponsePredicate "createContainsValidLocation" _ = ResponsePredicate "createContainsValidLocation" (\resp ->
getsHaveLastModifiedHeader :: Response b -> IO Bool getsHaveLastModifiedHeader :: Response b -> IO Bool
getsHaveLastModifiedHeader getsHaveLastModifiedHeader
@ -221,3 +227,8 @@ finishPredicates p req mgr = do
resps <- reqResps (reqPreds p) req mgr resps <- reqResps (reqPreds p) req mgr
let preds = reqPred (reqPreds p) <> respPreds p let preds = reqPred (reqPreds p) <> respPreds p
return $ mconcat [respPred preds r | r <- resps ] return $ mconcat [respPred preds r | r <- resps ]
-- * helpers
hasHeader :: SBS.ByteString -> Response b -> Bool
hasHeader hdr r = mk hdr `elem` (fst <$> responseHeaders r)

View File

@ -8,13 +8,17 @@ import Data.Proxy
import Servant import Servant
import Test.Hspec import Test.Hspec
import Test.QuickCheck import Test.QuickCheck
import Servant.API.Internal.Test.ComprehensiveAPI
import Servant.QuickCheck import Servant.QuickCheck
import Servant.QuickCheck.Internal (genRequest)
spec :: Spec spec :: Spec
spec = do spec = do
serversEqualSpec serversEqualSpec
serverSatisfiesSpec serverSatisfiesSpec
isComprehensiveSpec
serversEqualSpec :: Spec serversEqualSpec :: Spec
serversEqualSpec = describe "serversEqual" $ do serversEqualSpec = describe "serversEqual" $ do
@ -32,6 +36,17 @@ serverSatisfiesSpec = describe "serverSatisfies" $ do
withServantServer api server $ \burl -> withServantServer api server $ \burl ->
serverSatisfies api burl args (not500 <%> mempty) serverSatisfies api burl args (not500 <%> mempty)
it "fails for false predicates" $ do
withServantServer api server $ \burl ->
serverSatisfies api burl args (onlyJsonObjects <%> mempty)
isComprehensiveSpec :: Spec
isComprehensiveSpec = describe "HasGenRequest" $ do
it "has instances for all 'servant' combinators" $ do
let _g = genRequest comprehensiveAPI
True `shouldBe` True -- This is a type-level check
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
-- APIs -- APIs