{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TupleSections #-} module DecisionTests where import Control.Applicative import Control.Lens import Control.Monad.Catch.Pure import Control.Monad.Reader import Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy as LB import qualified Data.HashMap.Strict as HashMap import Data.Monoid import Network.HTTP.Date import Network.HTTP.Media import Network.HTTP.Types import Test.Tasty import Test.Tasty.HUnit import Webcrank import Webcrank.Internal import Webcrank.Internal.DecisionCore import Webcrank.Internal.Halt import TestServerAPI decisionTests :: TestTree decisionTests = testGroup "Decision tests" [ b13Tests , b12Tests , b11Tests , b10Tests , b9Tests , b8Tests , b7Tests , b6Tests , b5Tests , b4Tests , b3Tests , c3Tests , c4Tests , d4Tests , d5Tests , e5Tests , e6Tests , f6Tests , f7Tests , g7Tests , g8Tests , g9Tests , g11Tests , h7Tests , h10Tests , h11Tests , h12Tests , i4Tests , i7Tests , i12Tests , i13Tests , j18Tests , k5Tests , k7Tests , k13Tests , l5Tests , l7Tests , l13Tests , l14Tests , l15Tests , l17Tests , m5Tests , m7Tests , m16Tests , m20Tests , n5Tests , n11Tests , n16Tests , o14Tests , o16Tests , o18Tests , o20Tests , p3Tests , p11Tests ] b13Tests :: TestTree b13Tests = decisionTestGroup "b13" "Service available?" [ testCase "True ==> b12" $ after' b13 resource req @?= Decision' "b12" , testCase "False ==> 503 Service Unavailable" $ let r = resource { serviceAvailable = return False } in after' b13 r req @?= error' serviceUnavailable503 ] b12Tests :: TestTree b12Tests = decisionTestGroup "b12" "Known method?" ts where ts = mk <$> ms mk (m, r) = testCase (mconcat [show m, " ==> ", show r]) $ after' b12 resource (req { reqMethod = m }) @?= r ms = ("QWERTY", error' notImplemented501) : ((, Decision' "b11") <$> [methodGet, methodHead, methodPost, methodPut, methodDelete, methodTrace, methodConnect, methodOptions]) b11Tests :: TestTree b11Tests = decisionTestGroup "b11" "URI too long?" [ testCase "True ==> 414 Request-URI Too Long" $ let r = resource { uriTooLong = return True } in after' b11 r req @?= error' requestURITooLong414 , testCase "False ==> b11" $ after' b11 resource req @?= Decision' "b10" ] b10Tests :: TestTree b10Tests = decisionTestGroup "b10" "Method allowed?" [ testCase "True ==> b9" $ after' b10 resource req @?= Decision' "b9" , testCase "False ==> 405 Method Not Allowed" $ let rq = req { reqMethod = methodPost } in afterHdrs b10 resource rq @?= (error' methodNotAllowed405, HashMap.singleton hAllow ["GET, HEAD"]) ] b9Tests :: TestTree b9Tests = decisionTestGroup "b9" "Malformed?" [ testCase "True ==> 400 Malformed Request" $ let r = resource { malformedRequest = return True } in after' b9 r req @?= error' badRequest400 , testCase "False ==> b8" $ after' b9 resource req @?= Decision' "b8" ] b8Tests :: TestTree b8Tests = decisionTestGroup "b8" "Authorized?" [ testCase "True ==> b7" $ after' b8 resource req @?= Decision' "b7" , testCase "False ==> 401 Unauthorized" $ let r = resource { isAuthorized = return $ Unauthorized "Basic realm=\"W\"" } in afterHdrs b8 r req @?= (error' unauthorized401, HashMap.singleton hWWWAuthenticate ["Basic realm=\"W\""]) ] b7Tests :: TestTree b7Tests = decisionTestGroup "b7" "Forbidden?" [ testCase "True ==> 403 Forbidden" $ let r = resource { forbidden = return True } in after' b7 r req @?= error' forbidden403 , testCase "False ==> b6" $ after' b7 resource req @?= Decision' "b6" ] b6Tests :: TestTree b6Tests = decisionTestGroup "b6" "Okay Content-* Headers?" [ testCase "True ==> b5" $ after' b6 resource req @?= Decision' "b5" , testCase "False ==> 501 Not Implemented" $ let r = resource { validContentHeaders = return False } in after' b6 r req @?= error' notImplemented501 ] b5Tests :: TestTree b5Tests = decisionTestGroup "b5" "Known Content-Type?" [ testCase "True ==> b4" $ after' b5 resource req @?= Decision' "b4" , testCase "False ==> 415 Unsupported Media Type" $ let r = resource { knownContentType = return False } in after' b5 r req @?= error' unsupportedMediaType415 ] b4Tests :: TestTree b4Tests = decisionTestGroup "b4" "Req Entity Too Large?" [ testCase "True ==> b3" $ after' b4 resource req @?= Decision' "b3" , testCase "False ==> 413 Request Entity Too Large" $ let r = resource { validEntityLength = return False } in after' b4 r req @?= error' requestEntityTooLarge413 ] b3Tests :: TestTree b3Tests = decisionTestGroup "b3" "OPTIONS?" [ testCase "True ==> 200 Ok" $ let r = resource { options = return [("X-Tra", "Webcrank")] } rq = req { reqMethod = methodOptions } in afterHdrs b3 r rq @?= (Done' ok200, HashMap.singleton "X-Tra" ["Webcrank"]) , testCase "False ==> c3" $ after' b3 resource req @?= Decision' "c3" ] c3Tests :: TestTree c3Tests = decisionTestGroup "c3" "Accept exists?" [ testCase "True ==> c4 + default media type" $ let rq = req { reqHeaders = HashMap.singleton hAccept ["text/html"] } in afterMediaType c3 resource rq @?= (Decision' "c4", "application" // "octet-stream") , testCase "False ==> c4 w/o media type" $ let r = resource { contentTypesProvided = return [("text" // "html", return "")] } in afterMediaType c3 r req @?= (Decision' "d4", "text" // "html") ] where afterMediaType s r rq = case after s r rq of (d, rd) -> (d, _reqDataRespMediaType rd) c4Tests :: TestTree c4Tests = decisionTestGroup "c4" "Acceptable media type available?" [ testCase "True ==> d4" $ let r = resource { contentTypesProvided = return [("text" // "html", return "")] } in after' (c4 "text/html") r req @?= Decision' "d4" , testCase "False ==> 406 Not Acceptable" $ after' (c4 "text/html") resource req @?= Error' notAcceptable406 "No acceptable media type available" ] d4Tests :: TestTree d4Tests = decisionTestGroup "d4" "Accept-Language exists?" [ testCase "True ==> d5" $ let rq = req { reqHeaders = HashMap.singleton hAcceptLanguage ["en"] } in after' d4 resource rq @?= Decision' "d5" , testCase "False ==> e5" $ after' d4 resource req @?= Decision' "e5" ] -- just a placeholder d5Tests :: TestTree d5Tests = decisionTestGroup "d5" "Acceptable language available?" [ testCase "Always ==> e5" $ after' (d5 "en") resource req @?= Decision' "e5" ] e5Tests :: TestTree e5Tests = decisionTestGroup "e5" "Accept-Charset exists?" [ testCase "True ==> e6" $ let rq = req { reqHeaders = HashMap.singleton hAcceptCharset ["utf-8"] } in after' e5 resource rq @?= Decision' "e6" , testCase "False + no charsets provided ==> f6 w/o charset" $ afterCharset e5 resource req @?= (Decision' "f6", Nothing) , testCase "False + charset provided ==> f6 + charset" $ let r = resource { charsetsProvided = provideCharsets $ return ("utf-8", id) } in afterCharset e5 r req @?= (Decision' "f6", Just "utf-8") ] e6Tests :: TestTree e6Tests = decisionTestGroup "e6" "Acceptable Charset available?" [ testCase "True ==> f6" $ let r = resource { charsetsProvided = provideCharsets $ return ("utf-8", id) } in afterCharset (e6 "utf-8") r req @?= (Decision' "f6", Just "utf-8") , testCase "no charsets provided ==> f6 w/o charset" $ afterCharset (e6 "utf-8") resource req @?= (Decision' "f6", Nothing) , testCase "False ==> 406 Not Acceptable" $ let r = resource { charsetsProvided = provideCharsets $ return ("wtf-9", id) } in after' (e6 "utf-8") r req @?= Error' notAcceptable406 "No acceptable charset available" ] f6Tests :: TestTree f6Tests = decisionTestGroup "f6" "Accept-Encoding exists?" [ testCase "True ==> f7" $ let rq = req { reqHeaders = HashMap.singleton hAcceptEncoding ["gzip"] } in after' f6 resource rq @?= Decision' "f7" , testCase "False ==> g7" $ after' f6 resource req @?= Decision' "g7" ] f7Tests :: TestTree f7Tests = decisionTestGroup "f7" "Acceptable encoding available?" [ testCase "True ==> g7 + encoding" $ let r = resource { encodingsProvided = return [("gzip", id)] } in afterEnc (f7 "gzip") r req @?= (Decision' "g7", Just "gzip") , testCase "False ==> g7 + identity" $ afterEnc (f7 "gzip") resource req @?= (Decision' "g7", Nothing) ] where afterEnc s r rq = case after s r rq of (d, rd) -> (d, _reqDataRespEncoding rd) g7Tests :: TestTree g7Tests = decisionTestGroup "g7" "Resource exists?" [ testCase "True ==> g8" $ after' g7 resource req @?= Decision' "g8" , testCase "False ==> h7" $ let r = resource { resourceExists = return False } in after' g7 r req @?= Decision' "h7" ] g8Tests :: TestTree g8Tests = decisionTestGroup "g8" "If-Match exists?" [ testCase "True ==> g9" $ let rq = req { reqHeaders = HashMap.singleton hIfMatch ["webcrank"] } in after' g8 resource rq @?= Decision' "g9" , testCase "False ==> h10" $ after' g8 resource req @?= Decision' "h10" ] g9Tests :: TestTree g9Tests = decisionTestGroup "g9" "If-Match: * exists?" [ testCase "True ==> h10" $ after' (g9 "*") resource req @?= Decision' "h10" , testCase "False ==> g11" $ let rq = req { reqHeaders = HashMap.singleton hIfMatch ["webcrank"] } in after' (g9 "webcrank") resource rq @?= Decision' "g11" ] g11Tests :: TestTree g11Tests = decisionTestGroup "g11" "ETag in If-Match?" [ testCase "True ==> h10" $ let r = resource { generateETag = return $ StrongETag "webcrank" } in after' (g11 "\"webcrank\"") r req @?= Decision' "h10" , testCase "False (no ETag) ==> 412 Precondition Failed" $ after' (g11 "\"webcrank\"") resource req @?= error' preconditionFailed412 , testCase "False (mismatch ETag) ==> 412 Precondition Failed" $ let r = resource { generateETag = return $ StrongETag "webcrank123" } in after' (g11 "\"webcrank456\"") r req @?= error' preconditionFailed412 ] h7Tests :: TestTree h7Tests = decisionTestGroup "h7" "If-Match exists? (no existing resource)" [ testCase "True ==> 412 Precondition Failed" $ let rq = req { reqHeaders = HashMap.singleton hIfMatch ["webcrank"] } in after' h7 resource rq @?= error' preconditionFailed412 , testCase "False ==> i7" $ after' h7 resource req @?= Decision' "i7" ] h10Tests :: TestTree h10Tests = decisionTestGroup "h10" "If-Unmodified-Since exists?" [ testCase "True ==> h11" $ let rq = req { reqHeaders = HashMap.singleton hIfUnmodifiedSince [dateStr] } in after' h10 resource rq @?= Decision' "h11" , testCase "False ==> i12" $ after' h10 resource req @?= Decision' "i12" ] h11Tests :: TestTree h11Tests = decisionTestGroup "h11" "If-Unmodified-Since is valid date?" [ testCase "True ==> h12" $ after' (h11 dateStr) resource req @?= Decision' "h12" , testCase "False ==> i12" $ after' (h11 "not a date") resource req @?= Decision' "i12" ] h12Tests :: TestTree h12Tests = decisionTestGroup "h12" "Last-Modified > If-Unmodified-Since?" [ testCase "True ==> 412 Precondition Failed" $ after' (h12 $ date { hdDay = 10 }) r req @?= error' preconditionFailed412 , testCase "False ==> i12" $ after' (h12 $ date { hdDay = 20 }) r req @?= Decision' "i12" ] where r = resource { lastModified = return date } i4Tests :: TestTree i4Tests = decisionTestGroup "i4" "Moved permanently? (apply PUT to different URI)" [ testCase "True ==> 301 Moved Permanently" $ let loc = "http://example.com/abc" r = resource { movedPermanently = return loc } in afterHdrs i4 r req @?= (Done' movedPermanently301, HashMap.singleton hLocation [loc]) , testCase "False ==> p3" $ after' i4 resource req @?= Decision' "p3" ] i7Tests :: TestTree i7Tests = decisionTestGroup "i7" "PUT?" [ testCase "True ==> i4" $ let rq = req { reqMethod = methodPut } in after' i7 resource rq @?= Decision' "i4" , testCase "False ==> k7" $ after' i7 resource req @?= Decision' "k7" ] i12Tests :: TestTree i12Tests = decisionTestGroup "i12" "If-None-Match exists?" [ testCase "True ==> i13" $ let rq = req { reqHeaders = HashMap.singleton hIfNoneMatch ["webcrank"] } in after' i12 resource rq @?= Decision' "i13" , testCase "False ==> l13" $ after' i12 resource req @?= Decision' "l13" ] i13Tests :: TestTree i13Tests = decisionTestGroup "i13" "If-None-Match: * exists?" [ testCase "True ==> j18" $ after' (i13 "*") resource req @?= Decision' "j18" , testCase "False ==> k13" $ after' (i13 "webcrank") resource req @?= Decision' "k13" ] j18Tests :: TestTree j18Tests = decisionTestGroup "j18" "GET or HEAD? (resource exists)" [ testCase "True (GET) ==> 304 Not Modified" $ after' j18 resource req @?= Done' notModified304 , testCase "True (HEAD) ==> 304 Not Modified" $ let rq = req { reqMethod = methodHead } in after' j18 resource rq @?= Done' notModified304 , testCase "False ==> 412 Precondition Failed" $ let rq = req { reqMethod = methodPost } in after' j18 resource rq @?= error' preconditionFailed412 ] k5Tests :: TestTree k5Tests = decisionTestGroup "k5" "Moved permanently? (non-PUT variant)" [ testCase "True ==> 301 Moved Permanently" $ let uri = "http://example.com/abc" r = resource { movedPermanently = return uri } in afterHdrs k5 r req @?= (Done' movedPermanently301, HashMap.singleton hLocation [uri]) , testCase "False ==> l5" $ after' k5 resource req @?= Decision' "l5" ] k7Tests :: TestTree k7Tests = decisionTestGroup "k7" "Previously existed?" [ testCase "True ==> k5" $ let r = resource { previouslyExisted = return True } in after' k7 r req @?= Decision' "k5" , testCase "False ==> l7" $ after' k7 resource req @?= Decision' "l7" ] k13Tests :: TestTree k13Tests = decisionTestGroup "k13" "ETag in If-None-Match?" [ testCase "True ==> j18" $ let r = resource { generateETag = return $ StrongETag "webcrank" } in after' (k13 "\"webcrank\"") r req @?= Decision' "j18" , testCase "False (no ETag) ==> l13" $ after' (k13 "\"webcrank\"") resource req @?= Decision' "l13" , testCase "False (mismatch ETag) ==> l13" $ let r = resource { generateETag = return $ StrongETag "webcrank123" } in after' (k13 "\"webcrank456\"") r req @?= Decision' "l13" ] l5Tests :: TestTree l5Tests = decisionTestGroup "l5" "Moved temporarily?" [ testCase "True ==> 307 Moved Temporarily" $ let uri = "http://example.com/abc" r = resource { movedTemporarily = return uri } in afterHdrs l5 r req @?= (Done' temporaryRedirect307, HashMap.singleton hLocation [uri]) , testCase "False ==> m5" $ after' l5 resource req @?= Decision' "m5" ] l7Tests :: TestTree l7Tests = decisionTestGroup "l7" "POST? (no existing resource)" [ testCase "True ==> m7" $ let rq = req { reqMethod = methodPost } in after' l7 resource rq @?= Decision' "m7" , testCase "False ==> 404 Not Found" $ after' l7 resource req @?= error' notFound404 ] l13Tests :: TestTree l13Tests = decisionTestGroup "l17" "If-Modified-Since exists?" [ testCase "True ==> l14" $ let rq = req { reqHeaders = HashMap.singleton hIfModifiedSince [dateStr] } in after' l13 resource rq @?= Decision' "l14" , testCase "False ==> m16" $ after' l13 resource req @?= Decision' "m16" ] l14Tests :: TestTree l14Tests = decisionTestGroup "l14" "If-Modified-Since is a valid date?" [ testCase "True ==> l15" $ after' (l14 dateStr) resource req @?= Decision' "l15" , testCase "False ==> m16" $ after' (l14 "not a date") resource req @?= Decision' "m16" ] l15Tests :: TestTree l15Tests = decisionTestGroup "l15" "If-Modified-Since > Now?" [ testCase "True ==> m16" $ let rq = req { reqTime = date { hdDay = 10 } } in after' (l15 date) resource rq @?= Decision' "m16" , testCase "False ==> l17" $ after' (l15 date) resource req @?= Decision' "l17" ] l17Tests :: TestTree l17Tests = decisionTestGroup "l17" "Last-Modified > If-Modified-Since?" [ testCase "True ==> m16" $ let r = resource { lastModified = return $ date { hdDay = 20 } } in after' (l17 date) r req @?= Decision' "m16" , testCase "No last modified ==> m16" $ after' (l17 date) resource req @?= Decision' "m16" , testCase "False ==> 304 Not Modified" $ let r = resource { lastModified = return $ date { hdDay = 10 } } in after' (l17 date) r req @?= Done' notModified304 ] m5Tests :: TestTree m5Tests = decisionTestGroup "m5" "POST? (resource previously existed)" [ testCase "True ==> n5" $ let rq = req { reqMethod = methodPost } in after' m5 resource rq @?= Decision' "n5" , testCase "False ==> 410 Gone" $ after' m5 resource req @?= error' gone410 ] m7Tests :: TestTree m7Tests = decisionTestGroup "m7" "Server allows POST to missing resource?" [ testCase "True ==> n11" $ let r = resource { allowMissingPost = return True } in after' m7 r req @?= Decision' "n11" , testCase "False ==> 404 Not Found" $ after' m7 resource req @?= error' notFound404 ] m16Tests :: TestTree m16Tests = decisionTestGroup "m16" "DELETE?" [ testCase "True ==> m20" $ let rq = req { reqMethod = methodDelete } in after' m16 resource rq @?= Decision' "m20" , testCase "False ==> n16" $ after' m16 resource req @?= Decision' "n16" ] m20Tests :: TestTree m20Tests = decisionTestGroup "m20" "Delete enacted?" [ testCase "True + completed ==> n11" $ let r = resource { deleteResource = return True } in after' m20 r req @?= Decision' "n11" , testCase "True + not completed ==> 202 Accepted" $ let r = resource { deleteResource = return True, deleteCompleted = return False } in after' m20 r req @?= Done' accepted202 , testCase "False ==> 500 Internal Server Error" $ after' m20 resource req @?= error' internalServerError500 ] n5Tests :: TestTree n5Tests = decisionTestGroup "n5" "Server allows POST to missing resource? (resource did not exist previously)" [ testCase "True ==> n11" $ let r = resource { allowMissingPost = return True } in after' n5 r req @?= Decision' "n11" , testCase "False ==> 410 Gone" $ after' n5 resource req @?= error' gone410 ] n11Tests :: TestTree n11Tests = decisionTestGroup "n11" "Redirect?" [ testCase "True + created + content type accepted ==> 303 See Other" $ let r = resource { postAction = return . PostCreateRedir $ ["webcrank"] , contentTypesAccepted = return [("text" // "html", return ())] } rq = req { reqHeaders = HashMap.singleton hContentType ["text/html"] } aft = case after n11 r rq of (d, rd) -> (d, HashMap.lookup hLocation $_reqDataRespHeaders rd, _reqDataDispPath rd) in aft @?= (Done' seeOther303, Just ["http://example.com/webcrank"], ["webcrank"]) , testCase "True + created + no content type accepted ==> 415 Unsupported Media Type" $ let r = resource { postAction = return . PostCreateRedir $ ["webcrank"] , contentTypesAccepted = return [("text" // "html", return ())] } rq = req { reqHeaders = HashMap.singleton hContentType ["text/plain"] } in after' n11 r rq @?= error' unsupportedMediaType415 , testCase "True + process ==> 303 See Other" $ let r = resource { postAction = return $ PostProcessRedir $ return "http://example.com/webcrank" } in afterHdrs n11 r req @?= (Done' seeOther303, HashMap.singleton hLocation ["http://example.com/webcrank"]) , testCase "False + created + content type accepted ==> p11" $ let r = resource { postAction = return . PostCreate $ ["webcrank"] , contentTypesAccepted = return [("text" // "html", return ())] } rq = req { reqHeaders = HashMap.singleton hContentType ["text/html"] } in afterHdrs n11 r rq @?= (Decision' "p11", HashMap.singleton hLocation ["http://example.com/webcrank"]) , testCase "False + created + no content type accepted ==> p11" $ let r = resource { postAction = return . PostCreate $ ["webcrank"] , contentTypesAccepted = return [("text" // "html", return ())] } rq = req { reqHeaders = HashMap.singleton hContentType ["text/plain"] } in afterHdrs n11 r rq @?= (error' unsupportedMediaType415, HashMap.singleton hLocation ["http://example.com/webcrank"]) , testCase "False + process ==> p11" $ let r = resource { postAction = return $ PostProcess $ return () } in after' n11 r req @?= Decision' "p11" ] n16Tests :: TestTree n16Tests = decisionTestGroup "n16" "POST? (resource exists)" [ testCase "True ==> n11" $ let rq = req { reqMethod = methodPost } in after' n16 resource rq @?= Decision' "n11" , testCase "False ==> o16" $ after' n16 resource req @?= Decision' "o16" ] o14Tests :: TestTree o14Tests = decisionTestGroup "o14" "Conflict? (resource exists)" [ testCase "True ==> 409 Conflict" $ let r = resource { isConflict = return True } in after' o14 r req @?= error' conflict409 , testCase "False + no acceptable content type ==> 415 Unsupported Media Type" $ after' o14 resource req @?= error' unsupportedMediaType415 , testCase "False + acceptable content type ==> p11" $ let r = resource { contentTypesAccepted = return [("text" // "html", return ())] } rq = req { reqHeaders = HashMap.singleton hContentType ["text/html"] } in after' o14 r rq @?= Decision' "p11" ] o16Tests :: TestTree o16Tests = decisionTestGroup "o16" "PUT? (resource exists)" [ testCase "True ==> o14" $ let rq = req { reqMethod = methodPut } in after' o16 resource rq @?= Decision' "o14" , testCase "False ==> o18" $ after' o16 resource req @?= Decision' "o18" ] o18Tests :: TestTree o18Tests = decisionTestGroup "o18" "Multiple representations?" [ testCase "True ==> 300 Multiple Choices" $ let r = resource { multipleChoices = return True } in after' o18 r req @?= Done' multipleChoices300 , testCase "False ==> 200 Ok" $ after' o18 resource req @?= Done' ok200 ] o20Tests :: TestTree o20Tests = decisionTestGroup "o20" "Response includes an entity?" [ testCase "True ==> o18" $ let body rd = rd { _reqDataRespBody = Just "webcrank" } aft = fst $ afterWith o20 resource req body in aft @?= Decision' "o18" , testCase "False ==> 204 No Content" $ after' o20 resource req @?= Done' noContent204 ] p3Tests :: TestTree p3Tests = decisionTestGroup "p3" "Conflict? (resource doesn't exist)" [ testCase "True ==> 409 Conflict" $ let r = resource { isConflict = return True } in after' p3 r req @?= error' conflict409 , testCase "False + no acceptable content types ==> 415 Unsupported Media Type" $ after' p3 resource req @?= error' unsupportedMediaType415 , testCase "False + acceptable content type ==> p11" $ let r = resource { contentTypesAccepted = return [("text" // "html", return ())] } rq = req { reqHeaders = HashMap.singleton hContentType ["text/html"] } in after' p3 r rq @?= Decision' "p11" ] p11Tests :: TestTree p11Tests = decisionTestGroup "p11" "New resource?" [ testCase "True ==> 201 Created" $ let loc rd = rd { _reqDataRespHeaders = HashMap.singleton hLocation ["http://example.com/abc"] } aft = fst $ afterWith p11 resource req loc in aft @?= Done' created201 , testCase "False ==> o20" $ after' p11 resource req @?= Decision' "o20" ] decisionTestGroup :: TestName -> TestName -> [TestTree] -> TestTree decisionTestGroup s n = testGroup (s <> ". " <> n) date :: HTTPDate date = defaultHTTPDate { hdYear = 1994 , hdMonth = 11 , hdDay = 15 , hdHour = 8 , hdMinute = 12 , hdSecond = 31 , hdWkday = 2 } dateStr :: ByteString dateStr = formatHTTPDate date after :: FlowChart (HaltT TestCrank) Status -> Resource TestCrank -> Req -> (Decision', ReqData) after s r rq = afterWith s r rq id afterWith :: FlowChart (HaltT TestCrank) Status -> Resource TestCrank -> Req -> (ReqData -> ReqData) -> (Decision', ReqData) afterWith s r rq f = run where run = case runReader (runCatchT $ step s r f) rq of Left e -> error $ show e Right a -> a step :: FlowChart (HaltT TestCrank) Status -> Resource TestCrank -> (ReqData -> ReqData) -> TestState (Decision', ReqData) step s r f = do let rd = f newReqData case s of Decision _ a -> runTestCrank (runHaltT a) r rd >>= \case (Left (Halt sc), rd', _) -> return (Done' sc, rd') (Left (Error sc e), rd', _) -> return (Error' sc e, rd') (Right (Decision l _), rd', _) -> return (Decision' l, rd') (Right (Done a'), rd', _) -> runTestCrank (runHaltT a') r rd' <&> \case (Left (Halt sc), rd'', _) -> (Done' sc, rd'') (Left (Error sc e), rd'', _) -> (Error' sc e, rd'') (Right sc, rd'', _) -> (Done' sc, rd'') Done _ -> error "can't look past end state of flow chart" after' :: FlowChart (HaltT TestCrank) Status -> Resource TestCrank -> Req -> Decision' after' s r = fst . after s r afterHdrs :: FlowChart (HaltT TestCrank) Status -> Resource TestCrank -> Req -> (Decision', HeadersMap) afterHdrs s r rq = case after s r rq of (d, rd) -> (d, _reqDataRespHeaders rd) afterCharset :: FlowChart (HaltT TestCrank) Status -> Resource TestCrank -> Req -> (Decision', Maybe Charset) afterCharset s r rq = case after s r rq of (d, rd) -> (d, _reqDataRespCharset rd) data Decision' = Error' Status LB.ByteString | Done' Status | Decision' String deriving Eq error' :: Status -> Decision' error' s = Error' s (LB.fromStrict $ statusMessage s) instance Show Decision' where show = \case Error' sc e -> mconcat [ show $ statusCode sc , " " , B.unpack $ statusMessage sc , " - " , show . B.unpack . LB.toStrict $ e ] Done' sc -> mconcat [show $ statusCode sc, " ", B.unpack $ statusMessage sc ] Decision' l -> l