hedgehog-servant-0.0.0.1: test/Test/Hedgehog/Servant.hs
{-# LANGUAGE TemplateHaskell #-}
module Test.Hedgehog.Servant
( tests
) where
import Control.Monad (forM_)
import Data.Aeson (FromJSON, ToJSON, eitherDecode)
import Data.String (IsString)
import Data.Proxy (Proxy(..))
import Data.Text (Text, splitOn)
import Data.Text.Encoding (decodeUtf8)
import Data.Functor ((<&>))
import Data.Foldable (find)
import GHC.Generics (Generic)
import Servant.API (Capture, ReqBody, Header, JSON)
import Servant.API (QueryFlag, QueryParam, QueryParams)
import Servant.API (Post)
import Servant.API ((:>), (:<|>))
import Servant.Client (BaseUrl(..), Scheme(..))
import Network.HTTP.Client (Request(..), RequestBody(..))
import Network.HTTP.Types (methodPost)
import Hedgehog (Gen, Property, PropertyT)
import Hedgehog (annotate, annotateShow, checkParallel, discover, forAll)
import Hedgehog (failure, property, success)
import Hedgehog ((===))
import qualified Hedgehog.Gen as Gen
import Hedgehog.Servant (GenRequest(..), GList(..))
-- A typical cat:
data Cat = Cat
{ name :: Text
, color :: Text
}
deriving stock (Generic)
-- With ability to go back and forth to JSON:
instance ToJSON Cat
instance FromJSON Cat
-- And a simple generator to get random values of Cat:
genCat :: Gen Cat
genCat = Cat
<$> Gen.element [ "Kitty", "Misty" ]
<*> Gen.element [ "Red", "Green", "Blue" ]
-- Here's a simple API that allows posting a Cat in JSON to
--
-- POST /cats
type SimplestApi =
"my" :> "cats" :> ReqBody '[JSON] Cat :> Post '[JSON] ()
-- Generate a request to the Cat API from a base URL
catRequestGen :: BaseUrl -> Gen Request
catRequestGen baseUrl =
genRequest (Proxy @SimplestApi) (genCat :*: GNil) <&>
\makeReq -> makeReq baseUrl
defaultBaseUrl :: BaseUrl
defaultBaseUrl = BaseUrl
{ baseUrlScheme = Https
, baseUrlHost = "localhost"
, baseUrlPort = 8080
, baseUrlPath = ""
}
prop_check_cat_request :: Property
prop_check_cat_request = property $ do
forAll (catRequestGen defaultBaseUrl) >>= propCat
type CaptureApi =
"cats" :> Capture "pathParam" Text :> Capture "path2" Text :> Post '[JSON] ()
captureGen :: Gen Text -> BaseUrl -> Gen Request
captureGen txt baseUrl =
genRequest (Proxy @CaptureApi) (txt :*: GNil) <&>
\makeReq -> makeReq baseUrl
prop_check_capture_req :: Property
prop_check_capture_req =
let
pathParam :: IsString a => a
pathParam = "myPathParam"
in
property $ do
req <- forAll $ captureGen (pure pathParam) defaultBaseUrl
propBaseUrl req
method req === methodPost
path req === "/cats/" <> pathParam <> "/" <> pathParam
-- A typical dog:
data Dog = Dog
{ dogName :: Text
, dogHappy :: Bool
}
deriving stock (Generic)
-- With ability to go back and forth to JSON:
instance ToJSON Dog
instance FromJSON Dog
-- And a simple generator to get random values of Cat:
genDog :: Gen Dog
genDog = Dog
<$> Gen.element [ "Pluto", "Rowdy" ]
<*> Gen.bool
type AltApi =
"my" :> "cats" :> ReqBody '[JSON] Cat :> Post '[JSON] ()
:<|> "dogs" :> ReqBody '[JSON] Dog :> Post '[JSON] ()
-- Generate a request to the Cat API from a base URL
altRequestGen :: BaseUrl -> Gen Request
altRequestGen baseUrl =
genRequest (Proxy @AltApi) (genCat :*: genDog :*: GNil) <&>
\makeReq -> makeReq baseUrl
prop_check_alt_api :: Property
prop_check_alt_api = property $ do
req <- forAll (altRequestGen defaultBaseUrl)
case path req of
"/my/cats" -> propCat req
"/dogs" -> propDog req
badPath -> annotate "Bad path" >> annotateShow badPath >> failure
type HeaderApi = "cats" :> Header "Correlation-Id" Text :> Post '[JSON] ()
headerRequestGen :: BaseUrl -> Gen Request
headerRequestGen baseUrl =
let
textGen :: Gen Text
textGen = pure "some-corr-id"
in
genRequest (Proxy @HeaderApi) (textGen :*: GNil) <&>
\makeReq -> makeReq baseUrl
prop_has_correlation_id :: Property
prop_has_correlation_id = property $ do
req <- forAll (headerRequestGen defaultBaseUrl)
case find ((== "Correlation-Id") . fst) (requestHeaders req) of
Just _ -> success
Nothing -> failure
type QueryFlagApi =
"my" :> "cats" :> QueryFlag "foo" :> QueryFlag "fi" :> Post '[JSON] ()
queryFlagGen :: BaseUrl -> Gen Request
queryFlagGen baseUrl =
genRequest (Proxy @QueryFlagApi) GNil <&>
\makeReq -> makeReq baseUrl
prop_has_query_Flag :: Property
prop_has_query_Flag = property $ do
req <- forAll (queryFlagGen defaultBaseUrl)
queryString req === "foo&fi"
type QueryParamApi =
"my" :> "cats" :> QueryParam "foo" Text :> QueryParam "fi" Text :> Post '[JSON] ()
queryParamGen :: BaseUrl -> Gen Request
queryParamGen baseUrl =
let
paramGen :: Gen Text
paramGen = pure "bar"
in
genRequest (Proxy @QueryParamApi) (paramGen :*: GNil) <&>
\makeReq -> makeReq baseUrl
prop_has_query_param :: Property
prop_has_query_param = property $ do
req <- forAll (queryParamGen defaultBaseUrl)
queryString req === "foo=bar&fi=bar"
type QueryParamsApi =
"my" :> "cats" :> QueryParams "foo" Text :> Post '[JSON] ()
queryParamsGen :: BaseUrl -> Gen Request
queryParamsGen baseUrl =
let
paramGen :: Gen Text
paramGen = pure "bar"
in
genRequest (Proxy @QueryParamsApi) (paramGen :*: GNil) <&>
\makeReq -> makeReq baseUrl
prop_has_query_params :: Property
prop_has_query_params = property $ do
queryParams <-
decodeUtf8 . queryString <$>
forAll (queryParamsGen defaultBaseUrl)
forM_ (splitOn "&" queryParams) (=== "foo[]=bar")
propCat :: Request -> PropertyT IO ()
propCat req = do
propBaseUrl req
propContentTypeJson req
propDecodeJson (Proxy @Cat) req
method req === methodPost
path req === "/my/cats"
propDog :: Request -> PropertyT IO ()
propDog req = do
propBaseUrl req
propContentTypeJson req
propDecodeJson (Proxy @Dog) req
method req === methodPost
path req === "/dogs"
propBaseUrl :: Request -> PropertyT IO ()
propBaseUrl req = do
method req === methodPost
secure req === True
host req === "localhost"
port req === 8080
propContentTypeJson :: Request -> PropertyT IO ()
propContentTypeJson req =
let
headers = requestHeaders req
in
case find ((== "Content-Type"). fst) headers of
Just _ -> success
Nothing -> do
annotate "Couldn't find \"Content-Type\" header"
annotateShow headers
failure
propDecodeJson :: forall a. FromJSON a => Proxy a -> Request -> PropertyT IO ()
propDecodeJson _ req =
case requestBody req of
RequestBodyLBS (eitherDecode @a -> Right _) ->
success
RequestBodyLBS (eitherDecode @a -> Left err) ->
annotateShow err >> failure
_ -> failure
tests :: IO Bool
tests = checkParallel $$discover