@@ -7,11 +7,12 @@ import Data.Bifunctor (first)
7import qualified Data.ByteString as SBS
7import qualified Data.ByteString as SBS
8import qualified Data.ByteString.Char8 as SBSC
8import qualified Data.ByteString.Char8 as SBSC
9import qualified Data.ByteString.Lazy as LBS
9import qualified Data.ByteString.Lazy as LBS
10−import Data.CaseInsensitive (mk, foldedCase)
10+import Data.CaseInsensitive (foldCase, foldedCase, mk)
11import Data.Either (isRight)
11import Data.Either (isRight)
12import Data.List.Split (wordsBy)
12import Data.List.Split (wordsBy)
13import Data.Maybe (fromMaybe, isJust)
13import Data.Maybe (fromMaybe, isJust)
14−import Data.Monoid ((<>))
14+import Data.Semigroup (Semigroup (..))
15+import qualified Data.Text as T
15import Data.Time (UTCTime, defaultTimeLocale, parseTimeM,
16import Data.Time (UTCTime, defaultTimeLocale, parseTimeM,
16 rfc822DateFormat)
17 rfc822DateFormat)
17import GHC.Generics (Generic)
18import GHC.Generics (Generic)
@@ -42,7 +43,7 @@ import Servant.QuickCheck.Internal.ErrorTypes
42-- /Since 0.0.0.0/
43-- /Since 0.0.0.0/
43not500 :: ResponsePredicate
44not500 :: ResponsePredicate
44not500 = ResponsePredicate $ \resp ->
45not500 = ResponsePredicate $ \resp ->
45− when (responseStatus resp == status500) $ fail "not500"
46+ when (responseStatus resp == status500) $ throw $ PredicateFailure "not500" Nothing resp
47-- | [__Optional__]
48-- | [__Optional__]
@@ -83,15 +84,13 @@ notLongerThan maxAllowed
83-- /Since 0.0.0.0/
84-- /Since 0.0.0.0/
84onlyJsonObjects :: ResponsePredicate
85onlyJsonObjects :: ResponsePredicate
85onlyJsonObjects
86onlyJsonObjects
86− = ResponsePredicate (\resp -> case go resp of
87+ = ResponsePredicate (\resp -> do
87− Nothing -> throw $ PredicateFailure "onlyJsonObjects" Nothing resp
88− Just () -> return ())
91− ctyp <- lookup "content-type" (first foldedCase <$> responseHeaders r)
88+ case lookup "content-type" (first foldedCase <$> responseHeaders resp) of
92− when ("application/json" `SBS.isPrefixOf` ctyp) $ do
90+ Just ctype -> when ("application/json" `SBS.isPrefixOf` ctype) $ do
93− (_ :: Object) <- decode (responseBody r)
91+ case (decode (responseBody resp) :: Maybe Object) of
92+ Nothing -> throw $ PredicateFailure "onlyJsonObjects" Nothing resp
94− return ()
93+ Just _ -> return ())
96-- | __Optional__
95-- | __Optional__
@@ -119,12 +118,12 @@ createContainsValidLocation
119 resp <- httpLbs req mgr
118 resp <- httpLbs req mgr
120 if responseStatus resp == status201
119 if responseStatus resp == status201
121 then case lookup "Location" $ responseHeaders resp of
120 then case lookup "Location" $ responseHeaders resp of
122− Nothing -> fail n
121+ Nothing -> throw $ PredicateFailure n (Just req) resp
123 Just l -> case parseRequest $ SBSC.unpack l of
122 Just l -> case parseRequest $ SBSC.unpack l of
124− Nothing -> fail n
123+ Nothing -> throw $ PredicateFailure n (Just req) resp
125 Just x -> do
124 Just x -> do
126 resp2 <- httpLbs x mgr
125 resp2 <- httpLbs x mgr
127− status2XX resp2 n
126+ status2XX (Just req) resp2 n
128 return [resp, resp2]
127 return [resp, resp2]
129 else return [resp]
128 else return [resp]
@@ -188,14 +187,15 @@ getsHaveLastModifiedHeader
188notAllowedContainsAllowHeader :: RequestPredicate
187notAllowedContainsAllowHeader :: RequestPredicate
189notAllowedContainsAllowHeader
188notAllowedContainsAllowHeader
190 = RequestPredicate $ \req mgr -> do
189 = RequestPredicate $ \req mgr -> do
191− resp <- mapM (flip httpLbs mgr) $ [ req { method = renderStdMethod m }
190+ let reqs = [ req { method = renderStdMethod m } | m <- [minBound .. maxBound]
192− | m <- [minBound .. maxBound ]
193− , renderStdMethod m /= method req ]
191+ , renderStdMethod m /= method req ]
192+ resp <- mapM (flip httpLbs mgr) reqs
194− case filter pred' resp of
194+ case filter pred' (zip reqs resp) of
195− (x:_) -> throw $ PredicateFailure "notAllowedContainsAllowHeader" (Just req) x
195+ (x:_) -> throw $ PredicateFailure "notAllowedContainsAllowHeader" (Just $ fst x) (snd x)
196 [] -> return resp
196 [] -> return resp
198− pred' resp = responseStatus resp == status405 && not (hasValidHeader "Allow" go resp)
198+ pred' (_, resp) = responseStatus resp == status405 && not (hasValidHeader "Allow" go resp)
200 go x = all (\y -> isRight $ parseMethod $ SBSC.pack y)
200 go x = all (\y -> isRight $ parseMethod $ SBSC.pack y)
201 $ wordsBy (`elem` (", " :: [Char])) (SBSC.unpack x)
201 $ wordsBy (`elem` (", " :: [Char])) (SBSC.unpack x)
@@ -225,7 +225,7 @@ honoursAcceptHeader
225 sacc = fromMaybe "*/*" $ lookup "Accept" (requestHeaders req)
225 sacc = fromMaybe "*/*" $ lookup "Accept" (requestHeaders req)
226 if status100 < scode && scode < status300
226 if status100 < scode && scode < status300
227 then if isJust $ sctype >>= \x -> matchAccept [x] sacc
227 then if isJust $ sctype >>= \x -> matchAccept [x] sacc
228− then fail "honoursAcceptHeader"
228+ then throw $ PredicateFailure "honoursAcceptHeader" (Just req) resp
229 else return [resp]
229 else return [resp]
230 else return [resp]
230 else return [resp]
@@ -336,7 +336,29 @@ unauthorizedContainsWWWAuthenticate
336 = ResponsePredicate $ \resp ->
336 = ResponsePredicate $ \resp ->
337 if responseStatus resp == status401
337 if responseStatus resp == status401
338 then unless (hasValidHeader "WWW-Authenticate" (const True) resp) $
338 then unless (hasValidHeader "WWW-Authenticate" (const True) resp) $
339− fail "unauthorizedContainsWWWAuthenticate"
339+ throw $ PredicateFailure "unauthorizedContainsWWWAuthenticate" Nothing resp
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
340 else return ()
362 else return ()
342-- * Predicate logic
364-- * Predicate logic
@@ -354,9 +376,12 @@ newtype ResponsePredicate = ResponsePredicate
354 { getResponsePredicate :: Response LBS.ByteString -> IO ()
376 { getResponsePredicate :: Response LBS.ByteString -> IO ()
355 } deriving (Generic)
377 } deriving (Generic)
379+instance Semigroup ResponsePredicate where
380+ ResponsePredicate a <> ResponsePredicate b = ResponsePredicate $ \x -> a x >> b x
357instance Monoid ResponsePredicate where
382instance Monoid ResponsePredicate where
358 mempty = ResponsePredicate $ const $ return ()
383 mempty = ResponsePredicate $ const $ return ()
359− ResponsePredicate a `mappend` ResponsePredicate b = ResponsePredicate $ \x -> a x >> b x
384+ mappend = (<>)
361-- | A predicate that depends on both the request and the response.
386-- | A predicate that depends on both the request and the response.
@@ -368,7 +393,11 @@ newtype RequestPredicate = RequestPredicate
368-- TODO: This isn't actually a monoid
393-- TODO: This isn't actually a monoid
369instance Monoid RequestPredicate where
394instance Monoid RequestPredicate where
370 mempty = RequestPredicate (\r m -> httpLbs r m >>= \x -> return ([x]))
395 mempty = RequestPredicate (\r m -> httpLbs r m >>= \x -> return ([x]))
398+-- TODO: This isn't actually a monoid
399+instance Semigroup RequestPredicate where
371− RequestPredicate a `mappend` RequestPredicate b = RequestPredicate $ \r mgr ->
400+ RequestPredicate a <> RequestPredicate b = RequestPredicate $ \r mgr ->
372 liftM2 (<>) (a r mgr) (b r mgr)
401 liftM2 (<>) (a r mgr) (b r mgr)
374-- | A set of predicates. Construct one with 'mempty' and '<%>'.
403-- | A set of predicates. Construct one with 'mempty' and '<%>'.
@@ -377,10 +406,13 @@ data Predicates = Predicates
377 , responsePredicates :: ResponsePredicate
406 , responsePredicates :: ResponsePredicate
378 } deriving (Generic)
407 } deriving (Generic)
409+instance Semigroup Predicates where
410+ a <> b = Predicates (requestPredicates a <> requestPredicates b)
411+ (responsePredicates a <> responsePredicates b)
380instance Monoid Predicates where
413instance Monoid Predicates where
381 mempty = Predicates mempty mempty
414 mempty = Predicates mempty mempty
382− a `mappend` b = Predicates (requestPredicates a <> requestPredicates b)
415+ mappend = (<>)
383− (responsePredicates a <> responsePredicates b)
385class JoinPreds a where
417class JoinPreds a where
386 joinPreds :: a -> Predicates -> Predicates
418 joinPreds :: a -> Predicates -> Predicates
@@ -422,8 +454,8 @@ isRFC822Date s
422 Nothing -> False
454 Nothing -> False
423 Just (_ :: UTCTime) -> True
455 Just (_ :: UTCTime) -> True
425−status2XX :: Monad m => Response b -> String -> m ()
457+status2XX :: Monad m => Maybe Request -> Response LBS.ByteString -> T.Text -> m ()
426−status2XX r t
458+status2XX mreq resp t
427− | status200 <= responseStatus r && responseStatus r < status300
459+ | status200 <= responseStatus resp && responseStatus resp < status300
428 = return ()
460 = return ()
429− | otherwise = fail t
461+ | otherwise = throw $ PredicateFailure t mreq resp