Use GADTs for static guarantee you use right type of post data.

This commit is contained in:
Konstantine Rybnikov 2013-07-30 11:11:34 +03:00
parent eb7ad4e480
commit 3655af11d3

View File

@ -2,6 +2,7 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE GADTs #-}
{-|
Yesod.Test is a pragmatic framework for testing web applications built
using wai and persistent.
@ -165,8 +166,8 @@ getTestYesod = fmap yedSite ST.get
getResponse :: YesodExample site (Maybe SResponse)
getResponse = fmap yedResponse ST.get
data RequestBuilderData site = RequestBuilderData
{ rbdPostData :: RBDPostData
data RequestBuilderData site p = RequestBuilderData
{ rbdPostData :: Maybe (RBDPostData p)
, rbdResponse :: (Maybe SResponse)
, rbdMethod :: H.Method
, rbdSite :: site
@ -175,8 +176,9 @@ data RequestBuilderData site = RequestBuilderData
, rbdHeaders :: H.RequestHeaders
}
data RBDPostData = MultipleItemsPostData [RequestPart]
| BinaryPostData BSL8.ByteString
data RBDPostData a where
MultipleItemsPostData :: [RequestPart] -> RBDPostData [RequestPart]
BinaryPostData :: BSL8.ByteString -> RBDPostData BSL8.ByteString
-- | Request parts let us discern regular key/values from files sent in the request.
data RequestPart
@ -186,7 +188,7 @@ data RequestPart
-- | The RequestBuilder state monad constructs an url encoded string of arguments
-- to send with your requests. Some of the functions that run on it use the current
-- response to analize the forms that the server is expecting to receive.
type RequestBuilder site = ST.StateT (RequestBuilderData site) IO
type RequestBuilder site p = ST.StateT (RequestBuilderData site p) IO
-- | Start describing a Tests suite keeping cookies and a reference to the tested 'Application'
-- and 'ConnectionPool'
@ -372,14 +374,16 @@ printMatches query = do
liftIO $ hPutStrLn stderr $ show matches
-- | Add a parameter with the given name and value.
addPostParam :: T.Text -> T.Text -> RequestBuilder site ()
addPostParam :: T.Text -> T.Text -> RequestBuilder site [RequestPart] ()
addPostParam name value =
ST.modify $ \rbd -> rbd { rbdPostData = (addPostData (rbdPostData rbd)) }
where addPostData x@(BinaryPostData _) = x
addPostData (MultipleItemsPostData posts) =
MultipleItemsPostData $ ReqKvPart name value : posts
where addPostData :: Maybe (RBDPostData [RequestPart])
-> Maybe (RBDPostData [RequestPart])
addPostData (Just (MultipleItemsPostData posts)) =
Just $ MultipleItemsPostData $ ReqKvPart name value : posts
addPostData Nothing = Just $ MultipleItemsPostData $ [ReqKvPart name value]
addGetParam :: T.Text -> T.Text -> RequestBuilder site ()
addGetParam :: T.Text -> T.Text -> RequestBuilder site p ()
addGetParam name value = ST.modify $ \rbd -> rbd
{ rbdGets = (TE.encodeUtf8 name, Just $ TE.encodeUtf8 value)
: rbdGets rbd
@ -388,16 +392,20 @@ addGetParam name value = ST.modify $ \rbd -> rbd
-- | Add a file to be posted with the current request
--
-- Adding a file will automatically change your request content-type to be multipart/form-data
addFile :: T.Text -> FilePath -> T.Text -> RequestBuilder site ()
addFile :: T.Text -> FilePath -> T.Text -> RequestBuilder site [RequestPart] ()
addFile name path mimetype = do
contents <- liftIO $ BSL8.readFile path
ST.modify $ \rbd -> rbd { rbdPostData = (addPostData (rbdPostData rbd) contents) }
where addPostData x@(BinaryPostData _) _ = x
addPostData (MultipleItemsPostData posts) contents =
MultipleItemsPostData $ ReqFilePart name path contents mimetype : posts
where addPostData :: Maybe (RBDPostData [RequestPart])
-> BSL8.ByteString
-> Maybe (RBDPostData [RequestPart])
addPostData (Just (MultipleItemsPostData posts)) contents =
Just $ MultipleItemsPostData $ ReqFilePart name path contents mimetype : posts
addPostData Nothing contents =
Just $ MultipleItemsPostData $ [ReqFilePart name path contents mimetype]
-- This looks up the name of a field based on the contents of the label pointing to it.
nameFromLabel :: T.Text -> RequestBuilder site T.Text
nameFromLabel :: T.Text -> RequestBuilder site p T.Text
nameFromLabel label = do
mres <- fmap rbdResponse ST.get
res <-
@ -436,19 +444,19 @@ nameFromLabel label = do
(<>) :: T.Text -> T.Text -> T.Text
(<>) = T.append
byLabel :: T.Text -> T.Text -> RequestBuilder site ()
byLabel :: T.Text -> T.Text -> RequestBuilder site [RequestPart] ()
byLabel label value = do
name <- nameFromLabel label
addPostParam name value
fileByLabel :: T.Text -> FilePath -> T.Text -> RequestBuilder site ()
fileByLabel :: T.Text -> FilePath -> T.Text -> RequestBuilder site [RequestPart] ()
fileByLabel label path mime = do
name <- nameFromLabel label
addFile name path mime
-- | Lookup a _nonce form field and add it's value to the params.
-- Receives a CSS selector that should resolve to the form element containing the nonce.
addNonce_ :: Query -> RequestBuilder site ()
addNonce_ :: Query -> RequestBuilder site [RequestPart] ()
addNonce_ scope = do
matches <- htmlQuery' rbdResponse $ scope <> "input[name=_token][type=hidden][value]"
case matches of
@ -457,7 +465,7 @@ addNonce_ scope = do
_ -> failure $ "More than one nonce found in the page"
-- | For responses that display a single form, just lookup the only nonce available.
addNonce :: RequestBuilder site ()
addNonce :: RequestBuilder site [RequestPart] ()
addNonce = addNonce_ ""
-- | Perform a POST request to url
@ -486,12 +494,12 @@ get url = request $ do
setMethod "GET"
setUrl url
setMethod :: H.Method -> RequestBuilder site ()
setMethod :: H.Method -> RequestBuilder site p ()
setMethod m = ST.modify $ \rbd -> rbd { rbdMethod = m }
setUrl :: (Yesod site, RedirectUrl site url)
=> url
-> RequestBuilder site ()
-> RequestBuilder site p ()
setUrl url' = do
site <- fmap rbdSite ST.get
eurl <- runFakeHandler
@ -513,10 +521,10 @@ setUrl url' = do
-- | Simple way to set HTTP request body
setRequestBody :: (Yesod site)
=> BSL8.ByteString
-> RequestBuilder site ()
setRequestBody body = ST.modify $ \rbd -> rbd { rbdPostData = BinaryPostData body }
-> RequestBuilder site BSL8.ByteString ()
setRequestBody body = ST.modify $ \rbd -> rbd { rbdPostData = Just (BinaryPostData body) }
addRequestHeader :: H.Header -> RequestBuilder site ()
addRequestHeader :: H.Header -> RequestBuilder site p ()
addRequestHeader header = ST.modify $ \rbd -> rbd
{ rbdHeaders = header : rbdHeaders rbd
}
@ -524,13 +532,13 @@ addRequestHeader header = ST.modify $ \rbd -> rbd
-- | General interface to performing requests, allowing you to add extra
-- headers as well as letting you specify the request method.
request :: Yesod site
=> RequestBuilder site ()
=> RequestBuilder site p ()
-> YesodExample site ()
request reqBuilder = do
YesodExampleData app site oldCookies mRes <- ST.get
RequestBuilderData {..} <- liftIO $ ST.execStateT reqBuilder RequestBuilderData
{ rbdPostData = MultipleItemsPostData []
{ rbdPostData = Nothing
, rbdResponse = mRes
, rbdMethod = "GET"
, rbdSite = site
@ -546,11 +554,12 @@ request reqBuilder = do
cookiesForPath = M.filter (checkCookiePath path) cookies
let maker = case rbdPostData of
MultipleItemsPostData x ->
Just (MultipleItemsPostData x) ->
if DL.any isFile x
then makeMultipart
else makeSinglepart
BinaryPostData _ -> makeSinglepart
Just (BinaryPostData _) -> makeSinglepart
Nothing -> makeSinglepart
let req = maker cookiesForPath rbdPostData rbdMethod rbdHeaders path rbdGets
response <- liftIO $ runSession (srequest req) app
let newCookies = map (Cookie.parseSetCookie . snd) $ DL.filter (("Set-Cookie"==) . fst) $ simpleHeaders response
@ -572,11 +581,19 @@ request reqBuilder = do
boundary :: String
boundary = "*******noneedtomakethisrandom"
separator = BS8.concat ["--", BS8.pack boundary, "\r\n"]
makeMultipart :: M.Map a0 Cookie.SetCookie
-> Maybe (RBDPostData [RequestPart])
-> H.Method
-> [H.Header]
-> T.Text
-> H.Query
-> SRequest
makeMultipart cookies rbdPostData method extraHeaders urlPath urlQuery =
SRequest simpleRequest' (simpleRequestBody' rbdPostData)
where simpleRequestBody' (MultipleItemsPostData x) =
where simpleRequestBody' :: Maybe (RBDPostData [RequestPart]) -> BSL8.ByteString
simpleRequestBody' (Just (MultipleItemsPostData x)) =
BSL8.fromChunks [multiPartBody x]
simpleRequestBody' (BinaryPostData _) = ""
simpleRequestBody' Nothing = ""
simpleRequest' = mkRequest
[ ("Cookie", cookieValue)
, ("Content-Type", contentTypeValue)]
@ -600,7 +617,7 @@ request reqBuilder = do
-- For building the regular non-multipart requests
makeSinglepart :: M.Map a0 Cookie.SetCookie
-> RBDPostData
-> Maybe (RBDPostData p)
-> H.Method
-> [H.Header]
-> T.Text
@ -613,10 +630,11 @@ request reqBuilder = do
[ ("Cookie", cookieValue)
, ("Content-Type", "application/x-www-form-urlencoded")]
method extraHeaders urlPath urlQuery)
simpleRequestBody' (MultipleItemsPostData x) =
simpleRequestBody' (Just (MultipleItemsPostData x)) =
BSL8.fromChunks $ return $ TE.encodeUtf8 $ T.intercalate "&"
$ map singlepartPart x
simpleRequestBody' (BinaryPostData x) = x
simpleRequestBody' (Just (BinaryPostData x)) = x
simpleRequestBody' Nothing = ""
cookieValue = Builder.toByteString $ Cookie.renderCookies cookiePairs
cookiePairs = [ (Cookie.setCookieName c, Cookie.setCookieValue c)
| c <- map snd $ M.toList cookies ]