json-rpc-0.7.0.0: test/Network/JsonRpc/Tests.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE Rank2Types #-}
module Network.JsonRpc.Tests (tests) where
import Control.Applicative
import Control.Arrow
import Control.Concurrent.Async.Lifted
import Control.Concurrent.STM
import Control.Monad
import Control.Monad.Logger
import Control.Monad.Trans
import Control.Monad.Reader
import Data.Aeson
import Data.Aeson.Types
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as L
import Data.Conduit
import qualified Data.Conduit.List as CL
import Data.Conduit.TMChan
import qualified Data.Foldable as F
import Data.Function
import Data.List
import Data.Either
import qualified Data.HashMap.Strict as M
import Data.Maybe
import Data.Word
import Network.JsonRpc
import Test.QuickCheck
import Test.QuickCheck.Monadic
import Test.Framework
import Test.Framework.Providers.QuickCheck2
tests :: [Test]
tests =
[ testGroup "Messages"
[ testProperty "Check request fields"
(reqFields :: Request -> Bool)
, testProperty "Encode/decode request"
(testEncodeDecode :: Request -> Bool)
, testProperty "Check response fields"
(resFields :: Response -> Bool)
, testProperty "Encode/decode response"
(testEncodeDecode :: Response -> Bool)
, testProperty "Encode/decode message"
(testEncodeDecode :: Message -> Bool)
]
, testGroup "Conduits"
[ testProperty "Encoding" testEncodeConduit
, testProperty "Decoding" testDecodeConduit
, testProperty "Decode Garbage" testDecodeGarbageConduit
]
, testGroup "Functions"
[ testProperty "Processing incoming messages" testProcessIncoming
, testProperty "Processing incoming responses" testIncomingResponse
, testProperty "Receiving incoming requests" testReceiveRequest
, testProperty "Receiving incoming batch requests"
testReceiveBatchRequest
, testProperty "Sending responses" testSendResponse
, testProperty "Sending batch responses" testSendBatchResponse
, testProperty "Sending request batch" testSendBatchRequest
]
, testGroup "Network"
[ testProperty "Client/server talk" clientTest
, testProperty "Notifications" notifTest
]
]
data ReqList = ReqList { getReqList :: [Req] } deriving (Show, Eq)
instance Arbitrary ReqList where
arbitrary = resize 3 $ ReqList <$> arbitrary
data Req = Req { getReq :: Request } deriving (Show, Eq)
instance Arbitrary Req where
arbitrary = do
rq <- Request <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
return $ Req rq
data ReqNot = ReqNotReq { getReqNotParams :: Value }
| ReqNotNot { getReqNotParams :: Value }
deriving (Show, Eq)
instance ToJSON ReqNot where
toJSON = toJSON . getReqNotParams
instance Arbitrary ReqNot where
arbitrary = oneof [ ReqNotReq <$> arbitrary , ReqNotNot <$> arbitrary ]
instance ToRequest ReqNot where
requestMethod ReqNotReq{} = "request"
requestMethod ReqNotNot{} = "notification"
requestIsNotif ReqNotReq{} = False
requestIsNotif ReqNotNot{} = True
data Struct = Struct Value deriving (Show, Eq)
instance Arbitrary Struct where
arbitrary = resize 10 $ Struct <$> oneof [lsn, objn] where
nonull = oneof
[ toJSON <$> (arbitrary :: Gen String)
, toJSON <$> (arbitrary :: Gen Int)
, toJSON <$> (arbitrary :: Gen Double)
, toJSON <$> (arbitrary :: Gen Bool)
]
val = oneof [ nonull, return Null ]
ls = toJSON <$> listOf val
obj = toJSON . M.fromList <$> listOf ps
ps = (,) <$> (arbitrary :: Gen String) <*> oneof [val, ls]
lsn = toJSON <$> listOf (oneof [ls, obj, val])
objn = toJSON . M.fromList <$> listOf psn
psn = (,) <$> (arbitrary :: Gen String) <*> oneof [val, ls, obj]
reqFields :: Request -> Bool
reqFields rq = testFields (checkFieldsReq rq) rq
resFields :: Response -> Bool
resFields rs = testFields (checkFieldsRes rs) rs
fromRight :: Either a b -> b
fromRight (Right x) = x
fromRight _ = undefined
checkVerId :: Ver -> Maybe Id -> Object -> Parser Bool
checkVerId ver i o = do
j <- o .:? "jsonrpc"
guard $ if ver == V2 then j == Just (String "2.0") else isNothing j
o .:? "id" >>= guard . (==i)
return True
checkFieldsReqNotif
:: Ver -> Method -> Value -> Maybe Id -> Object -> Parser Bool
checkFieldsReqNotif ver m v i o = do
checkVerId ver i o >>= guard
o .: "method" >>= guard . (==m)
o .: "params" >>= guard . (==v)
return True
checkFieldsReq :: Request -> Object -> Parser Bool
checkFieldsReq (Request ver m v i) = checkFieldsReqNotif ver m v (Just i)
checkFieldsReq (Notif ver m v) = checkFieldsReqNotif ver m v Nothing
checkFieldsRes :: Response -> Object -> Parser Bool
checkFieldsRes (Response ver v i) o = do
checkVerId ver (Just i) o >>= guard
o .: "result" >>= guard . (==v)
return True
checkFieldsRes (ResponseError ver e i) o = do
checkVerId ver (Just i) o >>= guard
o .: "error" >>= guard . (==e)
return True
checkFieldsRes (OrphanError ver e) o = do
checkVerId ver Nothing o >>= guard
o .: "error" >>= guard . (==e)
return True
testFields :: ToJSON r => (Object -> Parser Bool) -> r -> Bool
testFields ck r = fromMaybe False . parseMaybe f $ toJSON r where
f = withObject "json" ck
testEncodeDecode :: (Eq r, ToJSON r, FromJSON r) => r -> Bool
testEncodeDecode r = maybe False (==r) $ parseMaybe parseJSON (toJSON r)
testEncodeConduit :: [Message] -> Property
testEncodeConduit msgs = monadicIO $ do
rt <- run $ runNoLoggingT $ do
bs <- CL.sourceList msgs =$ encodeConduit $$ CL.consume
return $ map decodeStrict' bs
assert $ msgs == map fromJust rt
testDecodeConduit :: ([Message], Ver) -> Property
testDecodeConduit (msgs, ver) = monadicIO $ do
rt <- run $ runNoLoggingT $
CL.sourceList (buffer encoded) =$ decodeConduit ver $$ CL.consume
assert $ rt == map (Right . toJSON) msgs
where
encoded = B.concat $ map (L.toStrict . encode) msgs
buffer bs | B.null bs = []
| otherwise = B.take 16 bs : buffer (B.drop 16 bs)
testDecodeGarbageConduit :: ([[Word8]], Ver) -> Property
testDecodeGarbageConduit (ss, ver) = monadicIO $ do
rt <- run $ runNoLoggingT $
CL.sourceList (map B.pack ss) =$ decodeConduit ver $$ CL.consume
assert $ all isLeft rt
testProcessIncoming :: ([Either Response Message], Ver, Bool)
-> Property
testProcessIncoming (msgs, ver, ignore) = monadicIO $ do
rt <- run $ runNoLoggingT $ do
expect <- liftIO . atomically $ newTMChan
qs <- liftIO . atomically $ initSession ver ignore
withAsync (cli qs expect msgs) $ \c -> do
runReaderT processIncoming qs
wait c
validate expect True
assert rt
where
validate expect state = do
newM <- liftIO . atomically $ readTMChan expect
case newM of
Just new -> validate expect $ new && state
Nothing -> return state
cli qs expect [] = liftIO . atomically $ do
closeTMChan expect
closeTBMChan $ inCh qs
cli qs expect (x:xs) = do
liftIO . atomically $ writeTBMChan (inCh qs) (toJSON <$> x)
liftIO . atomically $ case x of
Left e ->
caseLeftResponse qs e >>= writeTMChan expect
Right (MsgRequest req@Request{}) ->
caseSingleRequest req qs >>= writeTMChan expect
Right (MsgRequest nt@Notif{}) ->
caseSingleNotif nt qs >>= writeTMChan expect
Right MsgResponse{} ->
writeTMChan expect True
Right (MsgBatch []) ->
caseEmptyBatch qs >>= writeTMChan expect
Right (MsgBatch ms) ->
caseBatch qs ms >>= writeTMChan expect
cli qs expect xs
caseLeftResponse qs e = do
m <- readTBMChan $ outCh qs
case m of
Just (MsgResponse err) -> return $ err == e
_ -> return False
caseEmptyBatch qs = do
m <- readTBMChan $ outCh qs
case m of
Just (MsgResponse (OrphanError v (ErrorObj _ i _))) ->
return $ v == rpcVer qs && i == (-32600)
_ -> return False
caseBatch qs ms = do
let isrq (MsgRequest Request{}) = True
isrq _ = False
isnotif (MsgRequest Notif{}) = True
isnotif _ = False
if any isrq ms
then
if ignore
then do
m <- readTBMChan $ outCh qs
case m of
Just MsgBatch{} -> return True
_ -> return False
else do
m <- readTBMChan $ fromJust $ reqCh qs
case m of
Just BatchRequest{} -> return True
_ -> return False
else
if any isnotif ms
then
if ignore
then return True
else do
m <- readTBMChan $ fromJust $ reqCh qs
case m of
Just BatchRequest{} -> return True
_ -> return False
else return True
caseSingleRequest req qs =
if ignore
then do
m <- readTBMChan $ outCh qs
case m of
Just (MsgResponse (ResponseError _ e@ErrorObj{} _)) ->
return $ getErrCode e == (-32601)
_ -> return False
else do
m <- readTBMChan (fromJust $ reqCh qs)
return $ m == Just (SingleRequest req)
caseSingleNotif nt qs =
if ignore
then return True
else do
m <- readTBMChan (fromJust $ reqCh qs)
return $ m == Just (SingleRequest nt)
testIncomingResponse :: ([ReqList], Ver, Bool) -> Property
testIncomingResponse (reqss', ver, ignore) = nodup ==> monadicIO $ do
rt <- run $ runNoLoggingT $ do
expect <- liftIO . atomically $ newTMChan
qs <- liftIO . atomically $ do
qs <- initSession ver ignore
s <- sent
writeTVar (sentReqs qs) s
return qs
withAsync (cli qs expect msgs) $ \c -> do
runReaderT processIncoming qs
wait c
valid <- validate expect responses True
mapempty <- liftIO . atomically $ M.null <$> readTVar (sentReqs qs)
return $ valid && mapempty
assert rt
where
reqss = map getReqList reqss'
reqs = let f [q] = SingleRequest (getReq q)
f qs = BatchRequest (map getReq qs)
in map f reqss
flatreqs = let f (BatchRequest bt) = bt
f (SingleRequest rt) = [rt]
in concatMap f reqs
nodup = length (nubBy ((==) `on` getReqId) flatreqs) == length flatreqs
respond (Request v _ p i) = Response v p i
respond _ = undefined
responses = map respond flatreqs
msgs = map MsgResponse responses
sent = do
promises <- forM flatreqs $
\Request{getReqId = i} -> (,) i <$> newEmptyTMVar
return $ M.fromList promises
validate _ [] state = return state
validate expect (x:xs) state = do
newM <- liftIO . atomically $ readTMChan expect
case newM of
Just new -> validate expect xs (new == x && state)
Nothing -> return state
cli qs expect [] = liftIO . atomically $ do
closeTMChan expect
closeTBMChan $ inCh qs
cli qs expect (x:xs) =
case x of
MsgResponse res -> do
p <- getpromise qs res
liftIO . atomically $ writeTBMChan (inCh qs) (Right $ toJSON x)
liftIO . atomically $ do
rE <- readTMVar p
F.forM_ rE $ writeTMChan expect
cli qs expect xs
MsgBatch resps -> do
ps <- mapM (getpromise qs . getMsgResponse) resps
liftIO . atomically $ writeTBMChan (inCh qs) (Right $ toJSON x)
forM_ ps $ \p -> liftIO . atomically $ do
rE <- readTMVar p
F.forM_ rE $ writeTMChan expect
_ -> undefined
getpromise qs res = do
let i = getResId res
liftIO . atomically $ do
snt <- readTVar (sentReqs qs)
return . fromJust $ M.lookup i snt
testSendBatchRequest :: ([(ReqNot, Either ErrorObj Value)], Ver, Bool)
-> Property
testSendBatchRequest (reqnotres, ver, ignore) = nonull ==> monadicIO $ do
rs <- run $ runNoLoggingT $ do
qs <- liftIO . atomically $ initSession ver ignore
withAsync (cli qs) $ \c -> do
rs <- runReaderT (sendBatchRequest $ map fst reqnotres) qs
wait c
return (rs :: [Maybe (Either ErrorObj Value)])
assert $ length rs == length reqnotres
where
nonull = not $ null reqnotres
resmap = M.fromList $ map (first toJSON) reqnotres
respond (Request v _ _ i) (Left y) = Just $ ResponseError v y i
respond (Request v _ _ i) (Right y) = Just $ Response v y i
respond _ _ = undefined
fulfill req sent = F.forM_ (getReqId req `M.lookup` sent) $ \p ->
putTMVar p $ respond req $ fromJust $ getReqParams req `M.lookup` resmap
cli qs = liftIO . atomically $ do
msg <- readTBMChan (outCh qs)
sent <- readTVar (sentReqs qs)
case msg of
Just (MsgRequest req@Request{}) -> fulfill req sent
Just (MsgBatch bt) ->
forM_ bt $ \m ->
case m of
MsgRequest req@Request{} -> fulfill req sent
_ -> return ()
_ -> return ()
testReceiveRequest :: ([BatchRequest], Ver, Bool) -> Property
testReceiveRequest (reqs, ver, ignore) = monadicIO $ do
rt <- run $ runNoLoggingT $ do
qs <- liftIO . atomically $ initSession ver ignore
withAsync (send qs) $ \c -> do
res <- runReaderT receive qs
wait c
return res
assert $ and rt
where
send qs = F.forM_ (reqCh qs) $ \qch -> do
forM_ reqs $ liftIO . atomically . writeTBMChan qch
liftIO . atomically $ closeTBMChan qch
receive = forM reqs $ \q -> do
t <- receiveRequest
if ignore
then return $ isNothing t
else
case q of
SingleRequest{} ->
return $ Just q == fmap SingleRequest t
BatchRequest{} -> do
ch <- reader outCh
m <- liftIO . atomically $ readTBMChan ch
case m of
Just (MsgResponse OrphanError{}) ->
return $ isNothing t
_ -> return $ isNothing t
testReceiveBatchRequest :: ([BatchRequest], Ver, Bool) -> Property
testReceiveBatchRequest (reqs, ver, ignore) = monadicIO $ do
rt <- run $ runNoLoggingT $ do
qs <- liftIO . atomically $ initSession ver ignore
withAsync (send qs) $ \c -> do
rt <- runReaderT receive qs
wait c
return rt
if ignore
then assert $ all isNothing rt
else assert $ all isJust rt && map fromJust rt == reqs
where
send qs = F.forM_ (reqCh qs) $ \qch -> do
forM_ reqs $ liftIO . atomically . writeTBMChan qch
liftIO . atomically $ closeTBMChan qch
receive = forM reqs $ const receiveBatchRequest
testSendResponse :: ([Response], Ver, Bool) -> Property
testSendResponse (responses, ver, ignore) = monadicIO $ do
ex <- run $ runNoLoggingT $ do
qs <- liftIO . atomically $ initSession ver ignore
withAsync (receive qs) $ \c -> do
runReaderT send qs
wait c
assert $ length ex == length responses
assert $ all isJust ex && map fromJust ex == map MsgResponse responses
where
send = forM_ responses sendResponse
receive qs = forM responses $ const $ liftIO . atomically $
readTBMChan (outCh qs)
testSendBatchResponse :: ([BatchResponse], Ver, Bool) -> Property
testSendBatchResponse (responses, ver, ignore) = monadicIO $ do
ex <- run $ runNoLoggingT $ do
qs <- liftIO . atomically $ initSession ver ignore
withAsync (receive qs) $ \c -> do
runReaderT send qs
wait c
assert $ length ex == length responses
assert $ all isJust ex && map fromJust ex == map msg responses
where
msg (BatchResponse bt) = MsgBatch $ map MsgResponse bt
msg (SingleResponse r) = MsgResponse r
send = forM_ responses sendBatchResponse
receive qs = forM responses $ const $ liftIO . atomically $
readTBMChan (outCh qs)
createChans :: MonadIO m
=> m ((TBMChan a, TBMChan b), (Sink a m (), Source m b))
createChans = do
(bso, bsi) <- liftIO . atomically $ (,) <$> newTBMChan 16 <*> newTBMChan 16
let (snk, src) = (sinkTBMChan bso False, sourceTBMChan bsi)
return ((bso, bsi), (snk, src))
clientTest :: ([Value], Ver) -> Property
clientTest (qs, ver) = monadicIO $ do
rt <- run $ runNoLoggingT $ do
((bso, bsi), (snk, src)) <- createChans
let csnk = sinkTBMChan bsi False
csrc = sourceTBMChan bso
withAsync (server snk src) $ const $ cli csnk csrc
assert $ length rt == length qs
assert $ null rt || all correct rt
assert $ qs == results rt
where
respond q = return $ Right (q :: Value)
server snk src = runJsonRpcT ver False snk src (srv respond)
cli snk src = runJsonRpcT ver True snk src . forM qs $ sendRequest
results = map $ fromRight . fromJust
correct (Just (Right _)) = True
correct _ = False
notifTest :: ([Request], Ver) -> Property
notifTest (qs, ver) = monadicIO $ do
nt <- run $ runNoLoggingT $ do
((bso, bsi), (snk, src)) <- createChans
let csnk = sinkTBMChan bsi False
csrc = sourceTBMChan bso
(sig, notifs) <- liftIO . atomically $
(,) <$> newEmptyTMVar <*> newTVar []
withAsync (server snk src sig notifs) $ const $ cli sig csnk csrc
liftIO . atomically $ readTVar notifs
assert $ length nt == length ntfs
assert $ reverse nt == ntfs
where
respond q = return $ Right (q :: Value)
server snk src sig notifs =
runJsonRpcT ver False snk src (process sig notifs)
process sig notifs = do
qM <- receiveRequest
case qM of
Nothing -> return ()
Just q -> do
case q of
Notif{} -> liftIO . atomically $
readTVar notifs >>= writeTVar notifs . (q:)
Request{ getReqParams = String "disconnect" } ->
liftIO . atomically $ putTMVar sig ()
Request{} -> return ()
rM <- buildResponse respond q
case rM of
Nothing -> process sig notifs
Just r -> sendResponse r >> process sig notifs
reqs = map MsgRequest qs
cli sig snk src = runJsonRpcT ver True snk src $ do
forM_ reqs sendMessage
_ <- sendRequest $ String "disconnect"
:: JsonRpcT (NoLoggingT IO) (Maybe (Either ErrorObj Value))
liftIO . atomically $ takeTMVar sig
ntfs = flip filter qs $ \q -> case q of Notif{} -> True; _ -> False
srv :: (MonadLoggerIO m, FromRequest q, ToJSON r)
=> Respond q (JsonRpcT m) r -> JsonRpcT m ()
srv respond = do
qM <- receiveRequest
case qM of
Nothing -> return ()
Just q -> do
rM <- buildResponse respond q
case rM of
Nothing -> srv respond
Just r -> sendResponse r >> srv respond