@@ -1,6 +1,8 @@
1module Servant.QuickCheck.Internal.Predicates where
1module Servant.QuickCheck.Internal.Predicates where
3+import Control.Exception (catch, SomeException, throw)
3−import Control.Monad (liftM2)
4+import Control.Monad (liftM2, guard, ap)
5+import Control.Monad.Reader
4import Data.Aeson (Object, decode)
6import Data.Aeson (Object, decode)
5import Data.Bifunctor (Bifunctor (..))
7import Data.Bifunctor (Bifunctor (..))
6import Prelude.Compat
8import Prelude.Compat
@@ -24,6 +26,9 @@ import Network.HTTP.Types (methodGet, methodHead, parseMethod,
24 status300, status401, status405,
26 status300, status401, status405,
25 status500, status100)
27 status500, status100)
29+import Servant.QuickCheck.Internal.ErrorTypes
27-- | [__Best Practice__]
32-- | [__Best Practice__]
29-- @500 Internal Server Error@ should be avoided - it may represent some
34-- @500 Internal Server Error@ should be avoided - it may represent some
@@ -33,8 +38,9 @@ import Network.HTTP.Types (methodGet, methodHead, parseMethod,
33-- This function checks that the response code is not 500.
38-- This function checks that the response code is not 500.
35-- /Since 0.0.0.0/
40-- /Since 0.0.0.0/
36−not500 :: ResponsePredicate Text Bool
41+not500 :: ResponsePredicate
37−not500 = ResponsePredicate "not500" (\resp -> not $ responseStatus resp == status500)
42+not500 = ResponsePredicate $ \resp ->
43+ when (responseStatus resp == status500) $ fail "not500"
39-- | [__Best Practice__]
45-- | [__Best Practice__]
@@ -57,11 +63,11 @@ not500 = ResponsePredicate "not500" (\resp -> not $ responseStatus resp == statu
57-- * JSON Grammar: <https://tools.ietf.org/html/rfc4627#section-2 RFC 4627 Section 2>
63-- * JSON Grammar: <https://tools.ietf.org/html/rfc4627#section-2 RFC 4627 Section 2>
59-- /Since 0.0.0.0/
65-- /Since 0.0.0.0/
60−onlyJsonObjects :: ResponsePredicate Text Bool
66+onlyJsonObjects :: ResponsePredicate
61onlyJsonObjects
67onlyJsonObjects
62− = ResponsePredicate "onlyJsonObjects" (\resp -> case decode (responseBody resp) of
68+ = ResponsePredicate (\resp -> case decode (responseBody resp) of
63− Nothing -> False
69+ Nothing -> throw $ PredicateFailure "onlyJsonObjects" Nothing resp
64− Just (_ :: Object) -> True)
70+ Just (_ :: Object) -> return ())
66-- | __Optional__
72-- | __Optional__
@@ -82,25 +88,24 @@ onlyJsonObjects
82-- * Location header: <https://tools.ietf.org/html/rfc7231#section-7.1.2 RFC 7231 Section 7.1.2>
88-- * Location header: <https://tools.ietf.org/html/rfc7231#section-7.1.2 RFC 7231 Section 7.1.2>
84-- /Since 0.0.0.0/
90-- /Since 0.0.0.0/
85−createContainsValidLocation :: RequestPredicate Text Bool
91+createContainsValidLocation :: RequestPredicate
86createContainsValidLocation
92createContainsValidLocation
87− = RequestPredicate
93+ = RequestPredicate $ \req mgr -> do
88− { reqPredName = "createContainsValidLocation"
94+ let n = "createContainsValidLocation"
89− , reqResps = \req mgr -> do
90− resp <- httpLbs req mgr
95+ resp <- httpLbs req mgr
91− if responseStatus resp == status201
96+ if responseStatus resp == status201
92− then case lookup "Location" $ responseHeaders resp of
97+ then case lookup "Location" $ responseHeaders resp of
93− Nothing -> return (False, [resp])
98+ Nothing -> fail n
94− Just l -> case parseUrl $ SBSC.unpack l of
99+ Just l -> case parseUrl $ SBSC.unpack l of
95− Nothing -> return (False, [resp])
100+ Nothing -> fail n
96− Just x -> do
101+ Just x -> do
97− resp2 <- httpLbs x mgr
102+ resp2 <- httpLbs x mgr
98− return (status2XX resp2, [resp, resp2])
103+ status2XX resp2 n
104+ return [resp, resp2]
99− else return (True, [resp])
105+ else return [resp]
103−getsHaveLastModifiedHeader :: ResponsePredicate Text Bool
108+getsHaveLastModifiedHeader :: ResponsePredicate
104getsHaveLastModifiedHeader
109getsHaveLastModifiedHeader
105 = ResponsePredicate "getsHaveLastModifiedHeader" (\resp ->
110 = ResponsePredicate "getsHaveLastModifiedHeader" (\resp ->
@@ -122,18 +127,17 @@ getsHaveLastModifiedHeader
122-- * Status 405: <https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html RFC 2616 Section 10.4.6>
127-- * Status 405: <https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html RFC 2616 Section 10.4.6>
124-- /Since 0.0.0.0/
129-- /Since 0.0.0.0/
125−notAllowedContainsAllowHeader :: RequestPredicate Text Bool
130+notAllowedContainsAllowHeader :: RequestPredicate
126notAllowedContainsAllowHeader
131notAllowedContainsAllowHeader
127− = RequestPredicate
132+ = RequestPredicate $ \req mgr -> do
128− { reqPredName = "notAllowedContainsAllowHeader"
129− , reqResps = \req mgr -> do
130− resp <- mapM (flip httpLbs mgr) $ [ req { method = renderStdMethod m }
133+ resp <- mapM (flip httpLbs mgr) $ [ req { method = renderStdMethod m }
131− | m <- [minBound .. maxBound ]
134+ | m <- [minBound .. maxBound ]
132− , renderStdMethod m /= method req ]
135+ , renderStdMethod m /= method req ]
133− return (all pred' resp, resp)
136+ case filter pred' resp of
134− }
137+ (x:xs) -> throw $ PredicateFailure "notAllowedContainsAllowHeader" (Just req) x
136− pred' resp = responseStatus resp /= status405 || hasValidHeader "Allow" go resp
140+ pred' resp = responseStatus resp == status405 && not (hasValidHeader "Allow" go resp)
138 go x = all (\y -> isRight $ parseMethod $ SBSC.pack y)
142 go x = all (\y -> isRight $ parseMethod $ SBSC.pack y)
139 $ wordsBy (`elem` (", " :: [Char])) (SBSC.unpack x)
143 $ wordsBy (`elem` (", " :: [Char])) (SBSC.unpack x)
@@ -154,19 +158,19 @@ notAllowedContainsAllowHeader
154-- * @Accept@ header: <https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html RFC 2616 Section 14.1>
158-- * @Accept@ header: <https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html RFC 2616 Section 14.1>
156-- /Since 0.0.0.0/
160-- /Since 0.0.0.0/
157−honoursAcceptHeader :: RequestPredicate Text Bool
161+honoursAcceptHeader :: RequestPredicate
158honoursAcceptHeader
162honoursAcceptHeader
159− = RequestPredicate
163+ = RequestPredicate $ \req mgr -> do
160− { reqPredName = "honoursAcceptHeader"
161− , reqResps = \req mgr -> do
162− resp <- httpLbs req mgr
164+ resp <- httpLbs req mgr
163− let scode = responseStatus resp
165+ let scode = responseStatus resp
164− sctype = lookup "Content-Type" $ responseHeaders resp
166+ sctype = lookup "Content-Type" $ responseHeaders resp
165− sacc = fromMaybe "*/*" $ lookup "Accept" (requestHeaders req)
167+ sacc = fromMaybe "*/*" $ lookup "Accept" (requestHeaders req)
166− if status100 < scode && scode < status300
168+ if status100 < scode && scode < status300
167− then return (isJust $ sctype >>= \x -> matchAccept [x] sacc, [resp])
169+ then if isJust $ sctype >>= \x -> matchAccept [x] sacc
170+ then fail "honoursAcceptHeader"
168− else return (True, [resp])
172+ else return [resp]
171-- | [__Best Practice__]
175-- | [__Best Practice__]
@@ -182,34 +186,32 @@ honoursAcceptHeader
182-- * @Cache-Control@ header: <https://tools.ietf.org/html/rfc7234#section-5.2 RFC 7234 Section 5.2>
186-- * @Cache-Control@ header: <https://tools.ietf.org/html/rfc7234#section-5.2 RFC 7234 Section 5.2>
184-- /Since 0.0.0.0/
188-- /Since 0.0.0.0/
185−getsHaveCacheControlHeader :: RequestPredicate Text Bool
189+getsHaveCacheControlHeader :: RequestPredicate
186getsHaveCacheControlHeader
190getsHaveCacheControlHeader
187− = RequestPredicate
191+ = RequestPredicate $ \req mgr ->
188− { reqPredName = "getsHaveCacheControlHeader"
189− , reqResps = \req mgr -> if method req == methodGet
192+ if (method req == methodGet)
191− resp <- httpLbs req mgr
194+ resp <- httpLbs req mgr
192− let good = isJust $ lookup "Cache-Control" $ responseHeaders resp
195+ unless (hasValidHeader "Cache-Control" (const True) resp) $ do
193− return (good, [resp])
196+ throw $ PredicateFailure "getsHaveCacheControlHeader" (Just req) resp
194− else return (True, [])
197+ return [resp]
195− }
198+ else return []
197-- | [__Best Practice__]
200-- | [__Best Practice__]
199-- Like 'getsHaveCacheControlHeader', but for @HEAD@ requests.
202-- Like 'getsHaveCacheControlHeader', but for @HEAD@ requests.
201-- /Since 0.0.0.0/
204-- /Since 0.0.0.0/
202−headsHaveCacheControlHeader :: RequestPredicate Text Bool
205+headsHaveCacheControlHeader :: RequestPredicate
203headsHaveCacheControlHeader
206headsHaveCacheControlHeader
204− = RequestPredicate
207+ = RequestPredicate $ \req mgr ->
205− { reqPredName = "headsHaveCacheControlHeader"
206− , reqResps = \req mgr -> if method req == methodHead
208+ if (method req == methodHead)
208− resp <- httpLbs req mgr
210+ resp <- httpLbs req mgr
209− let good = hasValidHeader "Cache-Control" (const True) resp
211+ unless (hasValidHeader "Cache-Control" (const True) resp) $
210− return (good, [resp])
212+ throw $ PredicateFailure "headsHaveCacheControlHeader" (Just req) resp
211− else return (True, [])
214+ else return []
@@ -271,12 +273,13 @@ linkHeadersAreValid
271-- * @WWW-Authenticate@ header: <https://tools.ietf.org/html/rfc7235#section-4.1 RFC 7235 Section 4.1>
273-- * @WWW-Authenticate@ header: <https://tools.ietf.org/html/rfc7235#section-4.1 RFC 7235 Section 4.1>
273-- /Since 0.0.0.0/
275-- /Since 0.0.0.0/
274−unauthorizedContainsWWWAuthenticate :: ResponsePredicate Text Bool
276+unauthorizedContainsWWWAuthenticate :: ResponsePredicate
275unauthorizedContainsWWWAuthenticate
277unauthorizedContainsWWWAuthenticate
276− = ResponsePredicate "unauthorizedContainsWWWAuthenticate" (\resp ->
278+ = ResponsePredicate $ \resp ->
277 if responseStatus resp == status401
279 if responseStatus resp == status401
278− then hasValidHeader "WWW-Authenticate" (const True) resp
280+ then unless (hasValidHeader "WWW-Authenticate" (const True) resp) $
281+ fail "unauthorizedContainsWWWAuthenticate"
279− else True)
282+ else return ()
281-- * Predicate logic
284-- * Predicate logic
@@ -289,67 +292,46 @@ unauthorizedContainsWWWAuthenticate
289-- | A predicate that depends only on the response.
292-- | A predicate that depends only on the response.
291-- /Since 0.0.0.0/
294-- /Since 0.0.0.0/
292−data ResponsePredicate n r = ResponsePredicate
295+data ResponsePredicate = ResponsePredicate
294− , respPred :: Response LBS.ByteString -> r
296+ { getResponsePredicate :: Response LBS.ByteString -> IO ()
295− } deriving (Functor, Generic)
297+ } deriving (Generic)
297−instance Bifunctor ResponsePredicate where
299+instance Monoid ResponsePredicate where
298− first f (ResponsePredicate a b) = ResponsePredicate (f a) b
300+ mempty = ResponsePredicate $ const $ return ()
301−instance (Monoid n, Monoid r) => Monoid (ResponsePredicate n r) where
302− mempty = ResponsePredicate mempty mempty
303− a `mappend` b = ResponsePredicate
301+ ResponsePredicate a `mappend` ResponsePredicate b = ResponsePredicate $ \x -> a x >> b x
304− { respPredName = respPredName a <> respPredName b
305− , respPred = respPred a <> respPred b
308-- | A predicate that depends on both the request and the response.
303-- | A predicate that depends on both the request and the response.
310-- /Since 0.0.0.0/
305-- /Since 0.0.0.0/
311−data RequestPredicate n r = RequestPredicate
306+data RequestPredicate = RequestPredicate
313− , reqResps :: Request -> Manager -> IO (r, [Response LBS.ByteString])
307+ { getRequestPredicate :: Request -> Manager -> IO [Response LBS.ByteString]
314− } deriving (Generic, Functor)
308+ } deriving (Generic)
316−instance Bifunctor RequestPredicate where
317− first f (RequestPredicate a b) = RequestPredicate (f a) b
320-- TODO: This isn't actually a monoid
310-- TODO: This isn't actually a monoid
321−instance (Monoid n, Monoid r) => Monoid (RequestPredicate n r) where
311+instance Monoid RequestPredicate where
322− mempty = RequestPredicate mempty (\r m -> httpLbs r m >>= \x -> return (mempty, [x]))
312+ mempty = RequestPredicate (\r m -> httpLbs r m >>= \x -> return ([x]))
323− a `mappend` b = RequestPredicate
313+ RequestPredicate a `mappend` RequestPredicate b = RequestPredicate $ \r mgr ->
324− { reqPredName = reqPredName a <> reqPredName b
314+ liftM2 (<>) (a r mgr) (b r mgr)
325− , reqResps = \x m -> liftM2 (<>) (reqResps a x m) (reqResps b x m)
328-- | A set of predicates. Construct one with 'mempty' and '<%>'.
316-- | A set of predicates. Construct one with 'mempty' and '<%>'.
329−data Predicates n r = Predicates
317+data Predicates = Predicates
330− { reqPreds :: RequestPredicate n r
318+ { requestPredicates :: RequestPredicate
331− , respPreds :: ResponsePredicate n r
319+ , responsePredicates :: ResponsePredicate
332− } deriving (Generic, Functor)
320+ } deriving (Generic)
334−instance (Monoid n, Monoid r) => Monoid (Predicates n r) where
322+instance Monoid Predicates where
335 mempty = Predicates mempty mempty
323 mempty = Predicates mempty mempty
336− a `mappend` b = Predicates (reqPreds a <> reqPreds b) (respPreds a <> respPreds b)
324+ a `mappend` b = Predicates (requestPredicates a <> requestPredicates b)
337−
325+ (responsePredicates a <> responsePredicates b)
340class JoinPreds a where
327class JoinPreds a where
341− joinPreds :: a -> Predicates [Text] [Text] -> Predicates [Text] [Text]
328+ joinPreds :: a -> Predicates -> Predicates
343−instance JoinPreds (RequestPredicate Text Bool) where
344− joinPreds p (Predicates x y) = Predicates (go <> x) y
345− where go = let p' = first return p
346− in fmap (\z -> if z then [] else reqPredName p') p'
348−instance JoinPreds (ResponsePredicate Text Bool) where
330+instance JoinPreds (RequestPredicate ) where
349− joinPreds p (Predicates x y) = Predicates x (go <> y)
331+ joinPreds p (Predicates x y) = Predicates (p <> x) y
350− where go = let p' = first return p
351− in fmap (\z -> if z then [] else respPredName p') p'
333+instance JoinPreds (ResponsePredicate ) where
334+ joinPreds p (Predicates x y) = Predicates x (p <> y)
354-- | Adds a new predicate (either `ResponsePredicate` or `RequestPredicate`) to
336-- | Adds a new predicate (either `ResponsePredicate` or `RequestPredicate`) to
355-- the existing predicates.
337-- the existing predicates.
@@ -357,14 +339,17 @@ instance JoinPreds (ResponsePredicate Text Bool) where
357-- > not500 <%> onlyJsonObjects <%> empty
339-- > not500 <%> onlyJsonObjects <%> empty
359-- /Since 0.0.0.0/
341-- /Since 0.0.0.0/
360−(<%>) :: JoinPreds a => a -> Predicates [Text] [Text] -> Predicates [Text] [Text]
342+(<%>) :: JoinPreds a => a -> Predicates -> Predicates
361(<%>) = joinPreds
343(<%>) = joinPreds
362infixr 6 <%>
344infixr 6 <%>
364−finishPredicates :: Predicates [Text] [Text] -> Request -> Manager -> IO [Text]
346+finishPredicates :: Predicates -> Request -> Manager -> IO (Maybe PredicateFailure)
365−finishPredicates p req mgr = do
347+finishPredicates p req mgr = go `catch` \(e :: PredicateFailure) -> return $ Just e
366− (soFar, resps) <- reqResps (reqPreds p) req mgr
348+ where
367− return $ soFar <> mconcat [respPred (respPreds p) r | r <- resps]
349+ go = do
350+ resps <- getRequestPredicate (requestPredicates p) req mgr
351+ mapM_ (getResponsePredicate $ responsePredicates p) resps
369-- * helpers
354-- * helpers
@@ -373,5 +358,8 @@ hasValidHeader hdr p r = case lookup (mk hdr) (responseHeaders r) of
373 Nothing -> False
358 Nothing -> False
374 Just v -> p v
359 Just v -> p v
376−status2XX :: Response b -> Bool
361+status2XX :: Monad m => Response b -> String -> m ()
377−status2XX r = status200 <= responseStatus r && responseStatus r < status300
363+ | status200 <= responseStatus r && responseStatus r < status300
365+ | otherwise = fail t