packages feed

script-monad-0.0.4: test/Control/Monad/Script/Http/Test.hs

{-# LANGUAGE OverloadedStrings, RecordWildCards, Rank2Types, BangPatterns #-}
module Control.Monad.Script.Http.Test (
    tests
) where

import Control.Concurrent.MVar
import Control.Monad.Trans.Identity
import System.IO
import Data.Functor.Identity
  ( Identity(..) )
import Data.Proxy
import Data.String
import Data.ByteString.Lazy
  ( ByteString, pack, toStrict, fromChunks )
import Data.Text
  ( Text )
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Text.Extras
import Data.Typeable
import Data.List (isSuffixOf)
import Network.HTTP.Types
import Network.HTTP.Client
  ( HttpException(..), HttpExceptionContent(..) )

import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck
import Test.QuickCheck.Property
  ( failed, rejected )

import Control.Monad.Script.Http
import Data.MockIO

import Data.MockIO.FileSystem
import Data.MockIO.Test.Server


tests :: Int -> MVar () -> TestTree
tests num lock = testGroup "Http"
  [ localOption (QuickCheckTests $ 10 * num) $ testGroup "Mock IO"
    [ testGroup "Internal"
      [ testProperty "comment: return ()" $
        prop_comment_value pU pU pU pU (evalMockIO evalId) (toIO simpleMockWorld) undefined
      , testProperty "comment: state unchanged" $
        prop_comment_state pU pU pU pU (evalMockIO evalId) (toIO simpleMockWorld) undefined
      , testProperty "wait: return ()" $
        prop_wait_value pU pU pU pU (evalMockIO evalId) (toIO simpleMockWorld) undefined
      , testProperty "wait: state unchanged" $
        prop_wait_state pU pU pU pU (evalMockIO evalId) (toIO simpleMockWorld) undefined
      , testProperty "httpGet: return" $
        prop_httpGet pU pU pU pU (evalMockIO evalId) (toIO simpleMockWorld) undefined
      , testProperty "httpSilentGet: return" $
        prop_httpSilentGet pU pU pU pU (evalMockIO evalId) (toIO simpleMockWorld) undefined
      , testProperty "httpPost: return ()" $
        prop_httpPost pU pU pU pU (evalMockIO evalId) (toIO simpleMockWorld) undefined
      , testProperty "httpSilentPost: return ()" $
        prop_httpSilentPost pU pU pU pU (evalMockIO evalId) (toIO simpleMockWorld) undefined
      , testProperty "throwError: is caught" $
        prop_throwError pU pU pU pU (evalMockIO evalId) (toIO simpleMockWorld) undefined
      , testProperty "throwJsonError: is caught" $
        prop_throwJsonError pU pU pU pU (evalMockIO evalId) (toIO simpleMockWorld) undefined
      , testProperty "parseJson: valid" $
        prop_parseJson pU pU pU pU (evalMockIO evalId) (toIO simpleMockWorld) undefined
      , testProperty "lookupKeyJson: valid" $
        prop_lookupKeyJson pU pU pU pU (evalMockIO evalId) (toIO simpleMockWorld) undefined
      , testProperty "logEntries: value" $
        prop_logEntries_log pU pU pU pU (evalMockIO evalId) (toIO simpleMockWorld) undefined
      ]
    , testGroup "External"
      [ testProperty "comment: is logged" $
        prop_comment_write pU pU pU pU (evalMockIO evalId) (toIOs simpleMockWorld) undefined
      , testProperty "comment: silent" $
        prop_comment_silent pU pU pU pU (evalMockIO evalId) (toIOs simpleMockWorld) undefined
      , testProperty "wait: is logged" $
        prop_wait_write pU pU pU pU (evalMockIO evalId) (toIOs simpleMockWorld) undefined
      , testProperty "wait: silent" $
        prop_wait_silent pU pU pU pU (evalMockIO evalId) (toIOs simpleMockWorld) undefined
      , testProperty "httpGet: is logged" $
        prop_httpGet_write pU pU pU pU (evalMockIO evalId) (toIOs simpleMockWorld) undefined
      , testProperty "httpGet: json" $
        prop_httpGet_json pU pU pU pU (evalMockIO evalId) (toIOs simpleMockWorld) undefined
      , testProperty "httpGet: silent" $
        prop_httpGet_silent pU pU pU pU (evalMockIO evalId) (toIOs simpleMockWorld) undefined
      , testProperty "httpSilentGet: is not logged" $
        prop_httpSilentGet_write pU pU pU pU (evalMockIO evalId) (toIOs simpleMockWorld) undefined
      , testProperty "throwError: is logged" $
        prop_throwError_write pU pU pU pU (evalMockIO evalId) (toIOs simpleMockWorld) undefined
      , testProperty "logDebug: is logged" $
        prop_logDebug_write pU pU pU pU (evalMockIO evalId) (toIOs simpleMockWorld) undefined
      ]
    ]
  , localOption (QuickCheckTests num) $ testGroup "Real IO"
    [ testGroup "Internal"
      [ real_internal_tests_for pU pU pU pU
      , real_internal_tests_for pI pI pI pI
      ]
    ]
  ]

real_internal_tests_for
  :: ( Eq s, Eq e, Eq w
     , Show s, Show r, Show e, Show w
     , Arbitrary s, Arbitrary r, Arbitrary e, Arbitrary w
     , Typeable e, Typeable r, Typeable w, Typeable s
     )
  => Proxy e -> Proxy r -> Proxy w -> Proxy s -> TestTree
real_internal_tests_for pe pr pw ps =
  let
    label =
      "e: " ++ show (typeRep pe) ++ "; " ++
      "r: " ++ show (typeRep pr) ++ "; " ++
      "w: " ++ show (typeRep pw) ++ "; " ++
      "s: " ++ show (typeRep ps) ++ " "
  in
    testGroup label
      [ testProperty "comment: return ()" $
        prop_comment_value pe pr pw ps (evalIO evalId) runIdentityT undefined
      , testProperty "comment: state unchanged" $
        prop_comment_state pe pr pw ps (evalIO evalId) runIdentityT undefined
      , testProperty "wait: return ()" $
        prop_wait_value pe pr pw ps (evalIO evalId) runIdentityT undefined
      , testProperty "wait: state unchanged" $
        prop_wait_state pe pr pw ps (evalIO evalId) runIdentityT undefined
      , testProperty "httpGet: return" $
        prop_httpGet pe pr pw ps (evalIO evalId) runIdentityT undefined
      , testProperty "httpSilentGet: return" $
        prop_httpSilentGet pe pr pw ps (evalIO evalId) runIdentityT undefined
      , testProperty "httpPost: return ()" $
        prop_httpPost pe pr pw ps (evalIO evalId) runIdentityT undefined
      , testProperty "httpSilentPost: return ()" $
        prop_httpSilentPost pe pr pw ps (evalIO evalId) runIdentityT undefined
      , testProperty "throwError: is caught" $
        prop_throwError pe pr pw ps (evalIO evalId) runIdentityT undefined
      , testProperty "throwJsonError: is caught" $
        prop_throwJsonError pe pr pw ps (evalIO evalId) runIdentityT undefined
      , testProperty "parseJson: valid" $
        prop_parseJson pe pr pw ps (evalIO evalId) runIdentityT undefined
      , testProperty "lookupKeyJson: valid" $
        prop_lookupKeyJson pe pr pw ps (evalIO evalId) runIdentityT undefined
      , testProperty "logEntries: value" $
        prop_logEntries_log pe pr pw ps (evalIO evalId) runIdentityT undefined
      ]


pU :: Proxy ()
pU = Proxy

pI :: Proxy Int
pI = Proxy

toIO :: MockWorld s -> IdentityT (MockIO s) a -> IO a
toIO u (IdentityT (MockIO x)) = do
  let (a,_) = x u
  return a

toIOs :: MockWorld s -> IdentityT (MockIO s) a -> IO (a, MockWorld s)
toIOs u (IdentityT (MockIO x)) = do
  return $ x u

as :: a -> a -> a
as _ a = a

testEnv :: r -> R e w r
testEnv r = (trivialEnv r)
  { _logHandle = stdout
  , _logOptions = trivialLogOptions
    { _logSilent = True
    }
  }

jsonEnv :: r -> R e w r
jsonEnv r = (trivialEnv r)
  { _logHandle = stdout
  , _logOptions = trivialLogOptions
    { _logSilent = False
    , _logJson = True
    , _logColor = False
    }
  }

noisyEnv :: r -> R e w r
noisyEnv r = (trivialEnv r)
  { _logHandle = stdout
  , _logOptions = trivialLogOptions
    { _logSilent = False
    }
  }

prop_comment_value
  :: (Monad eff, Show e, Show w, Show s)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO (Either (E e) t, S s, W e w))
  -> HttpT e r w s p eff ()
  -> s -> r -> Text -> Property
prop_comment_value _ _ _ _ eval cond x s r msg =
  checkHttpTT (basicState s) (testEnv r) eval cond
    (hasValue (== ())) $
    as x $ do
      comment msg

prop_comment_state
  :: (Monad eff, Eq s, Show e, Show w, Show s)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO (Either (E e) t, S s, W e w))
  -> HttpT e r w s p eff ()
  -> s -> r -> Text -> Property
prop_comment_state _ _ _ _ eval cond x s r msg =
  checkHttpTT (basicState s) (testEnv r) eval cond
    (hasState (== s)) $
    as x $ do
      comment msg

prop_comment_write
  :: (Monad eff, Show e, Show w, Show s, Show u)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO ((Either (E e) t, S s, W e w), MockWorld u))
  -> HttpT e r w s p eff ()
  -> s -> r -> Text -> Property
prop_comment_write _ _ _ _ eval cond x s r str =
  let msg = T.filter (/= '\n') str in
  checkHttpTT (basicState s) (noisyEnv r) eval cond
    (hasWorld $ outputContains msg) $
    as x $ do
      comment msg

prop_comment_silent
  :: (Monad eff, Show e, Show w, Show s, Show u)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO ((Either (E e) t, S s, W e w), MockWorld u))
  -> HttpT e r w s p eff ()
  -> s -> r -> Text -> Property
prop_comment_silent _ _ _ _ eval cond x s r str =
  let msg = T.filter (/= '\n') str in
  checkHttpTT (basicState s) (testEnv r) eval cond
    (hasWorld outputIsEmpty) $
    as x $ do
      comment msg

hasWorld
  :: (MockWorld u -> Bool)
  -> (v, MockWorld u)
  -> Bool
hasWorld p (_,w) = p w

outputContains
  :: Text
  -> MockWorld u
  -> Bool
outputContains str world =
  case getLines (Right stdout) $ _files world of
    Nothing -> False
    Just ls -> any (T.isSuffixOf str) ls

outputIsEmpty
  :: MockWorld u -> Bool
outputIsEmpty world =
  _files world == emptyFileSystem

prop_wait_value
  :: (Monad eff, Show e, Show w, Show s)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO (Either (E e) t, S s, W e w))
  -> HttpT e r w s p eff ()
  -> s -> r -> Int -> Property
prop_wait_value _ _ _ _ eval cond x s r k =
  checkHttpTT (basicState s) (testEnv r) eval cond
    (hasValue (== ())) $
    as x $ do
      wait k

prop_wait_state
  :: (Monad eff, Eq s, Show e, Show w, Show s)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO (Either (E e) t, S s, W e w))
  -> HttpT e r w s p eff ()
  -> s -> r -> Int -> Property
prop_wait_state _ _ _ _ eval cond x s r k =
  checkHttpTT (basicState s) (testEnv r) eval cond
    (hasState (== s)) $
    as x $ do
      wait k

prop_wait_write
  :: (Monad eff, Show e, Show w, Show s, Show u)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO ((Either (E e) t, S s, W e w), MockWorld u))
  -> HttpT e r w s p eff ()
  -> s -> r -> Int -> Property
prop_wait_write _ _ _ _ eval cond x s r k =
  checkHttpTT (basicState s) (noisyEnv r) eval cond
    (hasWorld $ outputContains $ "Wait for " <> T.pack (show k) <> "μs") $
    as x $ do
      wait k

prop_wait_silent
  :: (Monad eff, Show e, Show w, Show s, Show u)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO ((Either (E e) t, S s, W e w), MockWorld u))
  -> HttpT e r w s p eff ()
  -> s -> r -> Int -> Property
prop_wait_silent _ _ _ _ eval cond x s r k =
  checkHttpTT (basicState s) (testEnv r) eval cond
    (hasWorld outputIsEmpty) $
    as x $ do
      wait k

prop_httpGet
  :: (Monad eff, Show e, Show w, Show s)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO (Either (E e) t, S s, W e w))
  -> HttpT e r w s p eff Int
  -> s -> r -> Property
prop_httpGet _ _ _ _ eval cond x s r =
  checkHttpTT (basicState s) (testEnv r) eval cond
    (hasValue (== 200)) $
    as x $ do
      response <- httpGet "http://example.com"
      return (statusCode $ _responseStatus response)

prop_httpGet_json
  :: (Monad eff, Show e, Show w, Show s, Show u)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO ((Either (E e) t, S s, W e w), MockWorld u))
  -> HttpT e r w s p eff ()
  -> s -> r -> Property
prop_httpGet_json _ _ _ _ eval cond x s r =
  checkHttpTT (basicState s) (jsonEnv r) eval cond
    (hasWorld $ outputContains "True") $
    as x $ do
      val1 <- httpGet "http://example.com/json"
        >>= (return . _responseBody)
        >>= parseJson
      val2 <- parseJson "{\"key\":\"value\"}"
      comment $ T.pack $ show $ val1 == val2
      return ()

prop_httpGet_write
  :: (Monad eff, Show e, Show w, Show s, Show u)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO ((Either (E e) t, S s, W e w), MockWorld u))
  -> HttpT e r w s p eff ()
  -> s -> r -> Property
prop_httpGet_write _ _ _ _ eval cond x s r =
  checkHttpTT (basicState s) (noisyEnv r) eval cond
    (hasWorld $ outputContains "GET http://example.com") $
    as x $ do
      httpGet "http://example.com"
      return ()

prop_httpGet_silent
  :: (Monad eff, Show e, Show w, Show s, Show u)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO ((Either (E e) t, S s, W e w), MockWorld u))
  -> HttpT e r w s p eff ()
  -> s -> r -> Property
prop_httpGet_silent _ _ _ _ eval cond x s r =
  checkHttpTT (basicState s) (testEnv r) eval cond
    (hasWorld outputIsEmpty) $
    as x $ do
      httpGet "http://example.com"
      return ()

prop_httpSilentGet
  :: (Monad eff, Show e, Show w, Show s)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO (Either (E e) t, S s, W e w))
  -> HttpT e r w s p eff ()
  -> s -> r -> Property
prop_httpSilentGet _ _ _ _ eval cond x s r =
  checkHttpTT (basicState s) (testEnv r) eval cond
    (hasValue (== ())) $
    as x $ do
      httpSilentGet "http://example.com"
      return ()

prop_httpSilentGet_write
  :: (Monad eff, Show e, Show w, Show s, Show u)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO ((Either (E e) t, S s, W e w), MockWorld u))
  -> HttpT e r w s p eff ()
  -> s -> r -> Property
prop_httpSilentGet_write _ _ _ _ eval cond x s r =
  checkHttpTT (basicState s) (noisyEnv r) eval cond
    (hasWorld $ not . outputContains "http://example.com") $
    as x $ do
      httpSilentGet "http://example.com"
      return ()

prop_httpPost
  :: (Monad eff, Show e, Show w, Show s)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO (Either (E e) t, S s, W e w))
  -> HttpT e r w s p eff Int
  -> s -> r -> Text -> Property
prop_httpPost _ _ _ _ eval cond x s r payload =
  checkHttpTT (basicState s) (testEnv r) eval cond
    (hasValue (== 200)) $
    as x $ do
      response <- httpPost "http://example.com" $
        fromChunks [T.encodeUtf8 payload]
      return (statusCode $ _responseStatus response)

prop_httpSilentPost
  :: (Monad eff, Show e, Show w, Show s)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO (Either (E e) t, S s, W e w))
  -> HttpT e r w s p eff Int
  -> s -> r -> Text -> Property
prop_httpSilentPost _ _ _ _ eval cond x s r payload =
  checkHttpTT (basicState s) (testEnv r) eval cond
    (hasValue (== 200)) $
    as x $ do
      response <- httpSilentPost "http://example.com" $
        fromChunks [T.encodeUtf8 payload]
      return (statusCode $ _responseStatus response)

prop_throwError
  :: (Monad eff, Eq e, Show e, Show w, Show s)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO (Either (E e) t, S s, W e w))
  -> HttpT e r w s p eff Int
  -> s -> r -> Int -> e -> Property
prop_throwError _ _ _ _ eval cond x s r k err =
  checkHttpTT (basicState s) (testEnv r) eval cond
    (hasValue (== k)) $
    as x $ do
      catchError (throwError err)
        (\e -> if e == err then return k else return (k+1))

prop_logDebug_write
  :: (Monad eff, Show e, Show w, Show s, Show u)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO ((Either (E e) t, S s, W e w), MockWorld u))
  -> HttpT e r w s p eff ()
  -> s -> r -> w -> Property
prop_logDebug_write _ _ _ _ eval cond x s r w =
  checkHttpTT (basicState s) (noisyEnv r) eval cond
    (hasWorld $ outputContains "LOG") $
    as x $ do
      logDebug w
      return ()

prop_logEntries_log
  :: (Monad eff, Eq w, Show e, Show w, Show s)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO (Either (E e) t, S s, W e w))
  -> HttpT e r w s p eff ()
  -> s -> r -> w -> w -> w -> Property
prop_logEntries_log _ _ _ _ eval cond x s r w1 w2 w3 =
  checkHttpTT (basicState s) (testEnv r) eval cond
    (hasLog $ \w -> [w1,w2,w3] == logEntries w) $
    as x $ do
      logDebug w1
      comment "hey!"
      logDebug w2
      logDebug w3
      return ()

prop_throwError_write
  :: (Monad eff, Show e, Show w, Show s, Show u)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO ((Either (E e) t, S s, W e w), MockWorld u))
  -> HttpT e r w s p eff ()
  -> s -> r -> e -> Property
prop_throwError_write _ _ _ _ eval cond x s r e =
  checkHttpTT (basicState s) (noisyEnv r) eval cond
    (hasWorld $ outputContains "ERROR") $
    as x $ do
      throwError e
      return ()

prop_throwJsonError
  :: (Monad eff, Show e, Show w, Show s)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO (Either (E e) t, S s, W e w))
  -> HttpT e r w s p eff Int
  -> s -> r -> Int -> JsonError -> Property
prop_throwJsonError _ _ _ _ eval cond x s r k err =
  checkHttpTT (basicState s) (testEnv r) eval cond
    (hasValue (== k)) $
    as x $ do
      catchJsonError (throwJsonError err)
        (\e -> if e == err then return k else return (k+1))

prop_parseJson
  :: (Monad eff, Show e, Show w, Show s)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO (Either (E e) t, S s, W e w))
  -> HttpT e r w s p eff ()
  -> s -> r -> Int -> Property
prop_parseJson _ _ _ _ eval cond x s r k =
  checkHttpTT (basicState s) (testEnv r) eval cond
    (hasValue (== ())) $
    as x $ do
      parseJson $ fromString $ "{ \"key\":" ++ show k ++ " }"
      return ()

prop_lookupKeyJson
  :: (Monad eff, Show e, Show w, Show s)
  => Proxy e -> Proxy r -> Proxy w -> Proxy s
  -> (forall u. P p u -> eff u)
  -> (forall e s w t. IdentityT eff (Either (E e) t, S s, W e w) -> IO (Either (E e) t, S s, W e w))
  -> HttpT e r w s p eff Int
  -> s -> r -> Int -> Property
prop_lookupKeyJson _ _ _ _ eval cond x s r k =
  checkHttpTT (basicState s) (testEnv r) eval cond
    (hasValue (== k)) $
    as x $ do
      obj <- parseJson $ fromString $ "{ \"key\":" ++ show k ++ " }"
      lookupKeyJson "key" obj >>= constructFromJson

hasValue
  :: (Show e) => (t -> Bool)
  -> (Either (E e) t, S s, W e w)
  -> Property
hasValue p (x,_,_) = case x of
  Left (E_Http (HttpExceptionRequest _ err)) ->
    case err of
      -- These errors are outside the control of this code, so it's better
      -- to reject cases that trigger them than to fail the whole suite.
      ConnectionTimeout -> property rejected
      InternalException _ -> property rejected

      -- These might be real errors. :)
      e -> counterexample (show e) failed
  Left e -> counterexample (show e) failed
  Right a -> property $ p a

hasLog
  :: (W e w -> Bool)
  -> (Either (E e) t, S s, W e w)
  -> Bool
hasLog p (_,_,w) = p w

hasState
  :: (s -> Bool)
  -> (Either (E e) t, S s, W e w)
  -> Bool
hasState p (_,s,_) = p $ _userState s

data Id a = Id { unId :: a }

evalId :: (Monad m) => Id a -> m a
evalId (Id a) = return a