@@ -7,11 +7,12 @@ import Data.Bifunctor (first)
77import qualified Data.ByteString as SBS
88import qualified Data.ByteString.Char8 as SBSC
99import qualified Data.ByteString.Lazy as LBS
10−import Data.CaseInsensitive (mk, foldedCase)
10+import Data.CaseInsensitive (foldCase, foldedCase, mk)
1111import Data.Either (isRight)
1212import Data.List.Split (wordsBy)
1313import Data.Maybe (fromMaybe, isJust)
1414import Data.Monoid ((<>))
15+import qualified Data.Text as T
1516import Data.Time (UTCTime, defaultTimeLocale, parseTimeM,
1617 rfc822DateFormat)
1718import GHC.Generics (Generic)
@@ -42,7 +43,7 @@ import Servant.QuickCheck.Internal.ErrorTypes
4243-- /Since 0.0.0.0/
4344not500 :: ResponsePredicate
4445not500 = ResponsePredicate $ \resp ->
45− when (responseStatus resp == status500) $ fail "not500"
46+ when (responseStatus resp == status500) $ throw $ PredicateFailure "not500" Nothing resp
4647
4748-- | [__Optional__]
4849--
@@ -119,12 +120,12 @@ createContainsValidLocation
119120 resp <- httpLbs req mgr
120121 if responseStatus resp == status201
121122 then case lookup "Location" $ responseHeaders resp of
122− Nothing -> fail n
123+ Nothing -> throw $ PredicateFailure n (Just req) resp
123124 Just l -> case parseRequest $ SBSC.unpack l of
124− Nothing -> fail n
125+ Nothing -> throw $ PredicateFailure n (Just req) resp
125126 Just x -> do
126127 resp2 <- httpLbs x mgr
127− status2XX resp2 n
128+ status2XX (Just req) resp2 n
128129 return [resp, resp2]
129130 else return [resp]
130131
@@ -225,7 +226,7 @@ honoursAcceptHeader
225226 sacc = fromMaybe "*/*" $ lookup "Accept" (requestHeaders req)
226227 if status100 < scode && scode < status300
227228 then if isJust $ sctype >>= \x -> matchAccept [x] sacc
228− then fail "honoursAcceptHeader"
229+ then throw $ PredicateFailure "honoursAcceptHeader" (Just req) resp
229230 else return [resp]
230231 else return [resp]
231232
@@ -336,7 +337,29 @@ unauthorizedContainsWWWAuthenticate
336337 = ResponsePredicate $ \resp ->
337338 if responseStatus resp == status401
338339 then unless (hasValidHeader "WWW-Authenticate" (const True) resp) $
339− fail "unauthorizedContainsWWWAuthenticate"
340+ throw $ PredicateFailure "unauthorizedContainsWWWAuthenticate" Nothing resp
341+ else return ()
342+
343+
344+-- | [__RFC Compliance__]
345+--
346+-- [An HTML] document will start with exactly this string: <!DOCTYPE html>
347+--
348+-- This function checks that HTML documents (those with `Content-Type: text/html...`)
349+-- include a DOCTYPE declaration at the top. We do not enforce capital case for the string `DOCTYPE`.
350+--
351+-- __References__:
352+--
353+-- * HTML5 Doctype: <https://tools.ietf.org/html/rfc7992#section-6.1 RFC 7992 Section 6.1>
354+-- /Since 0.3.0.0/
355+htmlIncludesDoctype :: ResponsePredicate
356+htmlIncludesDoctype
357+ = ResponsePredicate $ \resp ->
358+ if hasValidHeader "Content-Type" (SBS.isPrefixOf . foldCase $ "text/html") resp
359+ then do
360+ let htmlContent = foldCase . LBS.take 20 $ responseBody resp
361+ unless (LBS.isPrefixOf (foldCase "<!doctype html>") htmlContent) $
362+ throw $ PredicateFailure "htmlIncludesDoctype" Nothing resp
340363 else return ()
341364
342365-- * Predicate logic
@@ -422,8 +445,8 @@ isRFC822Date s
422445 Nothing -> False
423446 Just (_ :: UTCTime) -> True
424447
425−status2XX :: Monad m => Response b -> String -> m ()
426−status2XX r t
427− | status200 <= responseStatus r && responseStatus r < status300
448+status2XX :: Monad m => Maybe Request -> Response LBS.ByteString -> T.Text -> m ()
449+status2XX mreq resp t
450+ | status200 <= responseStatus resp && responseStatus resp < status300
428451 = return ()
429− | otherwise = fail t
452+ | otherwise = throw $ PredicateFailure t mreq resp