{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module Main (main) where import qualified Network.JsonRpc.Server as S import Network.JsonRpc.Server ((:+:) (..)) import Internal ( request, defaultRq, defaultRsp , defaultIdErrRsp, nullIdErrRsp , version, result, rpcErr, method , params, id', array, rspToIdString) import qualified TestParallelism import Data.List (sortBy) import qualified Data.Vector as V import Data.Function (on) import qualified Data.Aeson as A import Data.Aeson ((.=)) import qualified Data.Aeson.Types as A import qualified Data.HashMap.Strict as H import qualified Data.ByteString.Lazy.Char8 as LB import Control.Monad.Trans (liftIO) import Control.Monad.State (State, runState, lift, modify) import Control.Monad.Identity (Identity, runIdentity) import Test.HUnit hiding (State, Test) import Test.Framework (defaultMain, Test) import Test.Framework.Providers.HUnit (testCase) import Prelude hiding (subtract) #if !MIN_VERSION_base(4,8,0) import Control.Applicative ((<$>)) #endif main :: IO () main = defaultMain $ errorHandlingTests ++ otherTests errorHandlingTests :: [Test] errorHandlingTests = [ testCase "invalid JSON" $ let rsp = runIdentity $ S.call [] $ LB.pack "{" in removeErrMsg <$> (A.decode =<< rsp) @?= Just (nullIdErrRsp (-32700)) , testCase "invalid JSON-RPC" $ assertSubtractResponse (A.object ["id" .= A.Number 10]) $ nullIdErrRsp (-32600) , testCase "empty batch call" $ assertSubtractResponse A.emptyArray $ nullIdErrRsp (-32600) , testCase "invalid batch element" $ removeErrMsg <$> callSubtractMethods (array [A.Bool True]) @?= Just (array [nullIdErrRsp (-32600)]) , testCase "wrong request version" $ assertSubtractResponse (defaultRq `version` Just "1.0") $ nullIdErrRsp (-32600) , testCase "wrong id type" $ assertSubtractResponse (defaultRq `id'` (Just $ A.Bool True)) $ nullIdErrRsp (-32600) , testCase "method not found" $ assertSubtractResponse (defaultRq `method` "add") (defaultIdErrRsp (-32601)) , testCase "wrong method name capitalization" $ assertSubtractResponse (defaultRq `method` "Subtract") (defaultIdErrRsp (-32601)) , testCase "missing required named argument" $ assertInvalidParams $ defaultRq `params` Just (A.object ["a" .= A.Number 1, "y" .= A.Number 20]) , testCase "missing required unnamed argument" $ assertInvalidParams $ defaultRq `method` "flipped subtract" `params` Just (array [A.Number 0]) , testCase "wrong argument type" $ assertInvalidParams $ defaultRq `params` Just (A.object ["x" .= A.Number 1, "y" .= A.String "2"]) , testCase "extra unnamed arguments" $ assertInvalidParams $ defaultRq `params` Just (array $ map A.Number [1, 2, 3]) , let req = defaultRq `id'` Nothing `method` "12345" in testCase "invalid notification" $ callSubtractMethods req @?= Nothing ] otherTests :: [Test] otherTests = [ testCase "encode RPC error" $ A.toJSON (S.rpcError (-1) "error") @?= rpcErr Nothing (-1) "error" , let err = S.rpcErrorWithData 1 "my message" errData testError = rpcErr (Just $ A.toJSON errData) 1 "my message" errData = ('\x03BB', [True], ()) in testCase "encode RPC error with data" $ A.toJSON err @?= testError , testCase "batch request" testBatch , testCase "batch notifications" testBatchNotifications , testCase "allow missing version" testAllowMissingVersion , testCase "no arguments" $ assertGetTimeResponse Nothing , testCase "empty argument array" $ assertGetTimeResponse $ Just A.emptyArray , testCase "empty argument A.object" $ assertGetTimeResponse $ Just A.emptyObject , let req = defaultRq `params` Just args args = A.object ["x" .= A.Number 10, "y" .= A.Number 20, "z" .= A.String "extra"] rsp = defaultRsp `result` A.Number (-10) in testCase "allow extra named argument" $ assertSubtractResponse req rsp , let req = defaultRq `params` (Just $ A.object [("x1", A.Number 500), ("x", A.Number 1000)]) rsp = defaultRsp `result` A.Number 1000 in testCase "use default named argument" $ assertSubtractResponse req rsp , let req = defaultRq `params` (Just $ array [A.Number 4]) rsp = defaultRsp `result` A.Number 4 in testCase "use default unnamed argument" $ assertSubtractResponse req rsp , testCase "string request ID" $ assertEqualId $ A.String "ID 5" , testCase "null request ID" $ assertEqualId A.Null , testCase "parallelize tasks" TestParallelism.testParallelizingTasks ] assertSubtractResponse :: A.Value -> A.Value -> Assertion assertSubtractResponse rq expectedRsp = removeErrMsg <$> rsp @?= Just expectedRsp where rsp = callSubtractMethods rq assertEqualId :: A.Value -> Assertion assertEqualId i = assertSubtractResponse (defaultRq `id'` Just i) (defaultRsp `id'` Just i) assertInvalidParams :: A.Value -> Assertion assertInvalidParams req = assertSubtractResponse req (defaultIdErrRsp (-32602)) testBatch :: Assertion testBatch = sortBy (compare `on` rspToIdString) <$> response @?= Just expected where expected = [nullIdErrRsp (-32600), rsp i1 2, rsp i2 4] where rsp i x = defaultRsp `id'` Just i `result` A.Number x response = fromArray =<< (removeErrMsg <$> callSubtractMethods (array requests)) requests = [rq (Just i1) 10 8, rq (Just i2) 24 20, rq Nothing 15 1, defaultRq `version` Just (A.String "abc")] where rq i x y = defaultRq `id'` i `params` toArgs x y toArgs :: Int -> Int -> Maybe A.Value toArgs x y = Just $ A.object ["x" .= x, "y" .= y] i1 = A.Number 1 i2 = A.Number 2 fromArray (A.Array v) = Just $ V.toList v fromArray _ = Nothing testBatchNotifications :: Assertion testBatchNotifications = runState response 0 @?= (Nothing, 10) where response = S.call [incrementStateMethod] $ A.encode rq rq = replicate 10 $ request Nothing "increment" Nothing testAllowMissingVersion :: Assertion testAllowMissingVersion = callSubtractMethods requestNoVersion @?= (Just $ defaultRsp `result` A.Number 1) where requestNoVersion = defaultRq `version` Nothing `params` Just (A.object ["x" .= A.Number 1]) incrementStateMethod :: S.Method (State Int) incrementStateMethod = S.toMethod "increment" f () where f :: S.RpcResult (State Int) () f = lift $ modify (+1) assertGetTimeResponse :: Maybe A.Value -> Assertion assertGetTimeResponse args = passed @? "unexpected RPC response" where passed = (expected ==) <$> rsp expected = Just $ defaultRsp `result` A.Number 100 req = defaultRq `method` "get_time_seconds" `params` args rsp = callGetTimeMethod req callSubtractMethods :: A.Value -> Maybe A.Value callSubtractMethods req = let methods :: [S.Method Identity] methods = [subtractMethod, flippedSubtractMethod] rsp = S.call methods $ A.encode req in A.decode =<< runIdentity rsp callGetTimeMethod :: A.Value -> IO (Maybe A.Value) callGetTimeMethod req = let rsp = S.call [getTimeMethod] $ A.encode req in (A.decode =<<) <$> rsp subtractMethod :: S.Method Identity subtractMethod = S.toMethod "subtract" subtract (S.Required "x" :+: S.Optional "y" 0 :+: ()) flippedSubtractMethod :: S.Method Identity flippedSubtractMethod = S.toMethod "flipped subtract" (flip subtract) ps where ps = S.Optional "y" (-1000) :+: S.Required "x" :+: () subtract :: Int -> Int -> S.RpcResult Identity Int subtract x y = return (x - y) getTimeMethod :: S.Method IO getTimeMethod = S.toMethod "get_time_seconds" getTestTime () where getTestTime :: S.RpcResult IO Integer getTestTime = liftIO $ return 100 removeErrMsg :: A.Value -> A.Value removeErrMsg (A.Object rsp) = A.Object $ H.adjust removeMsg "error" rsp where removeMsg (A.Object err) = A.Object $ H.insert "message" "" $ H.delete "data" err removeMsg v = v removeErrMsg (A.Array rsps) = A.Array $ removeErrMsg `V.map` rsps removeErrMsg v = v