@@ -1,33 +1,30 @@
1module Servant.QuickCheck.Internal.Predicates where
1module Servant.QuickCheck.Internal.Predicates where
3−import Control.Exception (catch, throw)
3+import Control.Exception (catch, SomeException, throw)
4−import Control.Monad (liftM2, unless, when)
4+import Control.Monad (liftM2, guard, ap)
5+import Control.Monad.Reader
5import Data.Aeson (Object, decode)
6import Data.Aeson (Object, decode)
6−import Data.Bifunctor (first)
7+import Data.Bifunctor (Bifunctor (..))
7import qualified Data.ByteString as SBS
9import qualified Data.ByteString as SBS
8import qualified Data.ByteString.Char8 as SBSC
10import qualified Data.ByteString.Char8 as SBSC
9import qualified Data.ByteString.Lazy as LBS
11import qualified Data.ByteString.Lazy as LBS
10−import Data.CaseInsensitive (foldCase, foldedCase, mk)
12+import Data.CaseInsensitive (mk)
11import Data.Either (isRight)
13import Data.Either (isRight)
12import Data.List.Split (wordsBy)
14import Data.List.Split (wordsBy)
13import Data.Maybe (fromMaybe, isJust)
15import Data.Maybe (fromMaybe, isJust)
14−import Data.Semigroup (Semigroup (..))
16+import Data.Monoid ((<>))
15−import qualified Data.Text as T
17+import Data.Text (Text)
16−import Data.Time (UTCTime, defaultTimeLocale, parseTimeM,
18import GHC.Generics (Generic)
18import GHC.Generics (Generic)
19import Network.HTTP.Client (Manager, Request, Response, httpLbs,
19import Network.HTTP.Client (Manager, Request, Response, httpLbs,
20− method, parseRequest, requestHeaders,
20+ method, parseUrl, requestHeaders,
21 responseBody, responseHeaders,
21 responseBody, responseHeaders,
22 responseStatus)
22 responseStatus)
23import Network.HTTP.Media (matchAccept)
23import Network.HTTP.Media (matchAccept)
24import Network.HTTP.Types (methodGet, methodHead, parseMethod,
24import Network.HTTP.Types (methodGet, methodHead, parseMethod,
25− renderStdMethod, status100, status200,
25+ renderStdMethod, status200, status201,
26− status201, status300, status401,
26+ status300, status401, status405,
27− status405, status500)
27+ status500, status100)
29−import System.Clock (Clock (Monotonic), diffTimeSpec,
32import Servant.QuickCheck.Internal.ErrorTypes
29import Servant.QuickCheck.Internal.ErrorTypes
@@ -43,23 +40,7 @@ import Servant.QuickCheck.Internal.ErrorTypes
43-- /Since 0.0.0.0/
40-- /Since 0.0.0.0/
44not500 :: ResponsePredicate
41not500 :: ResponsePredicate
45not500 = ResponsePredicate $ \resp ->
42not500 = ResponsePredicate $ \resp ->
46− when (responseStatus resp == status500) $ throw $ PredicateFailure "not500" Nothing resp
43+ when (responseStatus resp == status500) $ fail "not500"
50−-- This function checks that the response from the server does not take longer
51−-- than the specified number of nanoseconds.
54−notLongerThan :: Integer -> RequestPredicate
55−notLongerThan maxAllowed
56− = RequestPredicate $ \req mgr -> do
57− start <- getTime Monotonic
58− resp <- httpLbs req mgr
59− end <- getTime Monotonic
60− when (toNanoSecs (end `diffTimeSpec` start) > maxAllowed) $
61− throw $ PredicateFailure "notLongerThan" (Just req) resp
64-- | [__Best Practice__]
45-- | [__Best Practice__]
@@ -84,13 +65,9 @@ notLongerThan maxAllowed
84-- /Since 0.0.0.0/
65-- /Since 0.0.0.0/
85onlyJsonObjects :: ResponsePredicate
66onlyJsonObjects :: ResponsePredicate
86onlyJsonObjects
67onlyJsonObjects
87− = ResponsePredicate (\resp -> do
88− case lookup "content-type" (first foldedCase <$> responseHeaders resp) of
90− Just ctype -> when ("application/json" `SBS.isPrefixOf` ctype) $ do
91− case (decode (responseBody resp) :: Maybe Object) of
68+ = ResponsePredicate (\resp -> case decode (responseBody resp) of
92− Nothing -> throw $ PredicateFailure "onlyJsonObjects" Nothing resp
69+ Nothing -> throw $ PredicateFailure "onlyJsonObjects" Nothing resp
93− Just _ -> return ())
70+ Just (_ :: Object) -> return ())
95-- | __Optional__
72-- | __Optional__
@@ -118,52 +95,21 @@ createContainsValidLocation
118 resp <- httpLbs req mgr
95 resp <- httpLbs req mgr
119 if responseStatus resp == status201
96 if responseStatus resp == status201
120 then case lookup "Location" $ responseHeaders resp of
97 then case lookup "Location" $ responseHeaders resp of
121− Nothing -> throw $ PredicateFailure n (Just req) resp
98+ Nothing -> fail n
122− Just l -> case parseRequest $ SBSC.unpack l of
99+ Just l -> case parseUrl $ SBSC.unpack l of
123− Nothing -> throw $ PredicateFailure n (Just req) resp
100+ Nothing -> fail n
124 Just x -> do
101 Just x -> do
125 resp2 <- httpLbs x mgr
102 resp2 <- httpLbs x mgr
126− status2XX (Just req) resp2 n
103+ status2XX resp2 n
127 return [resp, resp2]
104 return [resp, resp2]
128 else return [resp]
105 else return [resp]
130−-- | [__Optional__]
107+{-
132−-- The @Last-Modified@ header represents the time a resource was last
133−-- modified. It is used to drive caching and conditional requests.
135−-- When using this mechanism, the server adds the @Last-Modified@ header to
136−-- responses. Clients may then make requests with the @If-Modified-Since@
137−-- header to conditionally request resources. If the resource has not
138−-- changed since that date, the server responds with a status code of 304
139−-- (@Not Modified@) without a response body.
141−-- The @Last-Modified@ header can also be used in conjunction with the
142−-- @If-Unmodified-Since@ header to drive optimistic concurrency.
144−-- The @Last-Modified@ date must be in RFC 822 format.
148−-- * 304 Not Modified: <https://tools.ietf.org/html/rfc7232#section-4.1 RFC 7232 Section 4.1>
149−-- * Last-Modified header: <https://tools.ietf.org/html/rfc7232#section-2.2 RFC 7232 Section 2.2>
150−-- * If-Modified-Since header: <https://tools.ietf.org/html/rfc7232#section-3.3 RFC 7232 Section 3.3>
151−-- * If-Unmodified-Since header: <https://tools.ietf.org/html/rfc7232#section-3.4 RFC 7232 Section 3.4>
152−-- * Date format: <https://tools.ietf.org/html/rfc2616#section-3.3 RFC 2616 Section 3.3>
155−getsHaveLastModifiedHeader :: RequestPredicate
108+getsHaveLastModifiedHeader :: ResponsePredicate
156getsHaveLastModifiedHeader
109getsHaveLastModifiedHeader
157− = RequestPredicate $ \req mgr ->
110+ = ResponsePredicate "getsHaveLastModifiedHeader" (\resp ->
158− if (method req == methodGet)
160− resp <- httpLbs req mgr
161− unless (hasValidHeader "Last-Modified" isRFC822Date resp) $ do
162− throw $ PredicateFailure "getsHaveLastModifiedHeader" (Just req) resp
168-- | [__RFC Compliance__]
114-- | [__RFC Compliance__]
@@ -175,27 +121,23 @@ getsHaveLastModifiedHeader
175-- This function checks that every @405 Method Not Allowed@ response contains
121-- This function checks that every @405 Method Not Allowed@ response contains
176-- an @Allow@ header with a list of standard HTTP methods.
122-- an @Allow@ header with a list of standard HTTP methods.
178−-- Note that 'servant' itself does not currently set the @Allow@ headers.
180-- __References__:
124-- __References__:
182-- * @Allow@ header: <https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html RFC 2616 Section 14.7>
126-- * @Allow@ header: <https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html RFC 2616 Section 14.7>
183-- * 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>
184−-- * Servant Allow header issue: <https://github.com/haskell-servant/servant/issues/489 Issue #489>
186-- /Since 0.0.0.0/
129-- /Since 0.0.0.0/
187notAllowedContainsAllowHeader :: RequestPredicate
130notAllowedContainsAllowHeader :: RequestPredicate
188notAllowedContainsAllowHeader
131notAllowedContainsAllowHeader
189 = RequestPredicate $ \req mgr -> do
132 = RequestPredicate $ \req mgr -> do
190− let reqs = [ req { method = renderStdMethod m } | m <- [minBound .. maxBound]
133+ resp <- mapM (flip httpLbs mgr) $ [ req { method = renderStdMethod m }
134+ | m <- [minBound .. maxBound ]
191− , renderStdMethod m /= method req ]
135+ , renderStdMethod m /= method req ]
192− resp <- mapM (flip httpLbs mgr) reqs
194− case filter pred' (zip reqs resp) of
136+ case filter pred' resp of
195− (x:_) -> throw $ PredicateFailure "notAllowedContainsAllowHeader" (Just $ fst x) (snd x)
137+ (x:xs) -> throw $ PredicateFailure "notAllowedContainsAllowHeader" (Just req) x
196 [] -> return resp
138 [] -> return resp
198− pred' (_, resp) = responseStatus resp == status405 && not (hasValidHeader "Allow" go resp)
140+ pred' resp = responseStatus resp == status405 && not (hasValidHeader "Allow" go resp)
200 go x = all (\y -> isRight $ parseMethod $ SBSC.pack y)
142 go x = all (\y -> isRight $ parseMethod $ SBSC.pack y)
201 $ wordsBy (`elem` (", " :: [Char])) (SBSC.unpack x)
143 $ wordsBy (`elem` (", " :: [Char])) (SBSC.unpack x)
@@ -225,7 +167,7 @@ honoursAcceptHeader
225 sacc = fromMaybe "*/*" $ lookup "Accept" (requestHeaders req)
167 sacc = fromMaybe "*/*" $ lookup "Accept" (requestHeaders req)
226 if status100 < scode && scode < status300
168 if status100 < scode && scode < status300
227 then if isJust $ sctype >>= \x -> matchAccept [x] sacc
169 then if isJust $ sctype >>= \x -> matchAccept [x] sacc
228− then throw $ PredicateFailure "honoursAcceptHeader" (Just req) resp
170+ then fail "honoursAcceptHeader"
229 else return [resp]
171 else return [resp]
230 else return [resp]
172 else return [resp]
@@ -336,29 +278,7 @@ unauthorizedContainsWWWAuthenticate
336 = ResponsePredicate $ \resp ->
278 = ResponsePredicate $ \resp ->
337 if responseStatus resp == status401
279 if responseStatus resp == status401
338 then unless (hasValidHeader "WWW-Authenticate" (const True) resp) $
280 then unless (hasValidHeader "WWW-Authenticate" (const True) resp) $
339− throw $ PredicateFailure "unauthorizedContainsWWWAuthenticate" Nothing resp
281+ fail "unauthorizedContainsWWWAuthenticate"
343−-- | [__RFC Compliance__]
345−-- [An HTML] document will start with exactly this string: <!DOCTYPE html>
347−-- This function checks that HTML documents (those with `Content-Type: text/html...`)
348−-- include a DOCTYPE declaration at the top. We do not enforce capital case for the string `DOCTYPE`.
352−-- * HTML5 Doctype: <https://tools.ietf.org/html/rfc7992#section-6.1 RFC 7992 Section 6.1>
354−htmlIncludesDoctype :: ResponsePredicate
356− = ResponsePredicate $ \resp ->
357− if hasValidHeader "Content-Type" (SBS.isPrefixOf . foldCase $ "text/html") resp
359− let htmlContent = foldCase . LBS.take 20 $ responseBody resp
360− unless (LBS.isPrefixOf (foldCase "<!doctype html>") htmlContent) $
361− throw $ PredicateFailure "htmlIncludesDoctype" Nothing resp
362 else return ()
282 else return ()
364-- * Predicate logic
284-- * Predicate logic
@@ -372,47 +292,37 @@ htmlIncludesDoctype
372-- | A predicate that depends only on the response.
292-- | A predicate that depends only on the response.
374-- /Since 0.0.0.0/
294-- /Since 0.0.0.0/
375−newtype ResponsePredicate = ResponsePredicate
295+data ResponsePredicate = ResponsePredicate
376 { getResponsePredicate :: Response LBS.ByteString -> IO ()
296 { getResponsePredicate :: Response LBS.ByteString -> IO ()
377 } deriving (Generic)
297 } deriving (Generic)
379−instance Semigroup ResponsePredicate where
380− ResponsePredicate a <> ResponsePredicate b = ResponsePredicate $ \x -> a x >> b x
382instance Monoid ResponsePredicate where
299instance Monoid ResponsePredicate where
383 mempty = ResponsePredicate $ const $ return ()
300 mempty = ResponsePredicate $ const $ return ()
384− mappend = (<>)
301+ ResponsePredicate a `mappend` ResponsePredicate b = ResponsePredicate $ \x -> a x >> b x
386-- | A predicate that depends on both the request and the response.
303-- | A predicate that depends on both the request and the response.
388-- /Since 0.0.0.0/
305-- /Since 0.0.0.0/
389−newtype RequestPredicate = RequestPredicate
306+data RequestPredicate = RequestPredicate
390− { getRequestPredicate :: Request -> Manager -> IO [Response LBS.ByteString]
307+ { getRequestPredicate :: Request -> Manager -> IO [Response LBS.ByteString]
391 } deriving (Generic)
308 } deriving (Generic)
393-- TODO: This isn't actually a monoid
310-- TODO: This isn't actually a monoid
394instance Monoid RequestPredicate where
311instance Monoid RequestPredicate where
395 mempty = RequestPredicate (\r m -> httpLbs r m >>= \x -> return ([x]))
312 mempty = RequestPredicate (\r m -> httpLbs r m >>= \x -> return ([x]))
398−-- TODO: This isn't actually a monoid
399−instance Semigroup RequestPredicate where
400− RequestPredicate a <> RequestPredicate b = RequestPredicate $ \r mgr ->
313+ RequestPredicate a `mappend` RequestPredicate b = RequestPredicate $ \r mgr ->
401 liftM2 (<>) (a r mgr) (b r mgr)
314 liftM2 (<>) (a r mgr) (b r mgr)
403-- | A set of predicates. Construct one with 'mempty' and '<%>'.
316-- | A set of predicates. Construct one with 'mempty' and '<%>'.
404data Predicates = Predicates
317data Predicates = Predicates
405− { requestPredicates :: RequestPredicate
318+ { requestPredicates :: RequestPredicate
406 , responsePredicates :: ResponsePredicate
319 , responsePredicates :: ResponsePredicate
407 } deriving (Generic)
320 } deriving (Generic)
409−instance Semigroup Predicates where
410− a <> b = Predicates (requestPredicates a <> requestPredicates b)
411− (responsePredicates a <> responsePredicates b)
413instance Monoid Predicates where
322instance Monoid Predicates where
414 mempty = Predicates mempty mempty
323 mempty = Predicates mempty mempty
415− mappend = (<>)
324+ a `mappend` b = Predicates (requestPredicates a <> requestPredicates b)
325+ (responsePredicates a <> responsePredicates b)
417class JoinPreds a where
327class JoinPreds a where
418 joinPreds :: a -> Predicates -> Predicates
328 joinPreds :: a -> Predicates -> Predicates
@@ -448,14 +358,8 @@ hasValidHeader hdr p r = case lookup (mk hdr) (responseHeaders r) of
448 Nothing -> False
358 Nothing -> False
449 Just v -> p v
359 Just v -> p v
451−isRFC822Date :: SBS.ByteString -> Bool
453− = case parseTimeM True defaultTimeLocale rfc822DateFormat (SBSC.unpack s) of
455− Just (_ :: UTCTime) -> True
457−status2XX :: Monad m => Maybe Request -> Response LBS.ByteString -> T.Text -> m ()
361+status2XX :: Monad m => Response b -> String -> m ()
458−status2XX mreq resp t
362+status2XX r t
459− | status200 <= responseStatus resp && responseStatus resp < status300
363+ | status200 <= responseStatus r && responseStatus r < status300
460 = return ()
364 = return ()
461− | otherwise = throw $ PredicateFailure t mreq resp
365+ | otherwise = fail t