packages feed

jsonrpc-conduit-0.4.0: test/Data/Conduit/JsonRpc/ServerSpec.hs

{- |
Module      :  Data.Conduit.JsonRpc.ServerSpec
Description :  Tests for the server.
Copyright   :  (c) 2015-2023 Gabriele Sales
-}

{-# LANGUAGE NamedFieldPuns    #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ParallelListComp  #-}

module Data.Conduit.JsonRpc.ServerSpec
  ( spec )
where

import           Control.Monad
import           Data.Aeson                          hiding (Error)
import           Data.Aeson.Parser                   (json')
import           Data.Aeson.Types                    (parseEither)
import           Data.Attoparsec.Combinator          (many')
import           Data.Bifunctor
import qualified Data.ByteString                     as S
import qualified Data.ByteString.Lazy                as L
import           Data.Conduit
import qualified Data.Conduit.Attoparsec             as A
import qualified Data.Conduit.Binary                 as B
import           Data.Conduit.JsonRpc.Internal.Types
import           Data.Conduit.JsonRpc.Methods
import           Data.Conduit.JsonRpc.Server
import           Data.Int
import           Data.Text                           (Text)
import           Test.Hspec


spec :: Spec
spec = do
  describe "method sum" $ do
    it "computes the sum" $ do
      let nums = [1,2,3] :: [Int]
      oneshot "sum" nums `shouldReturn` Right (6::Int)
    it "rejects strings" $ do
      let strings = ["1","2","3"] :: [Text]
      oneshot "sum" strings `shouldReturn`
        (Left "Invalid params" :: Either Text Int)

  describe "method cat" $
    it "concatenates strings" $ do
      let strings = ["a","b","c"] :: [Text]
      oneshot "cat" strings `shouldReturn` Right ("abc"::Text)

  describe "unknown method" $
    it "is rejected" $
      oneshot "unknown" () `shouldReturn`
        (Left "Method not found" :: Either Text ())

  describe "truncated request" $
    it "is rejected" $
      let req = truncatedRequest "sum" ([]::[Text])
      in oneshot' req `shouldReturn`
        (Left "Parse error" :: Either Text ())

  describe "arbitrary JSON value" $
    it "is rejected" $
      let req = encode (toJSON ["a"::Text])
      in oneshot' req `shouldReturn`
        (Left "Invalid request" :: Either Text ())

  describe "multiple requests" $
    it "are processed in order" $
      let reqs = [ ("sum", toJSON [1::Int, 2])
                 , ("cat", toJSON ["a"::Text, "c"])
                 ]
      in multishot reqs `shouldReturn`
           (Right [toJSON (3::Int), toJSON ("ac"::Text)])


oneshot :: (ToJSON a, FromJSON b) => Text -> a -> IO (Either Text b)
oneshot method params =
  let reqId = toJSON (1::Int)
  in  oneshot' (encode $ Request method params reqId)

oneshot' :: FromJSON a => L.ByteString -> IO (Either Text a)
oneshot' req = do
  bs <- runConduit (B.sourceLbs req .| server .| B.sinkLbs)
  return $ case decode bs of
             Nothing             -> Left "invalid response"
             Just Error{errMsg}  -> Left errMsg
             Just Result{result} -> Right result

truncatedRequest :: ToJSON a => Text -> a -> L.ByteString
truncatedRequest method params =
  let reqId = toJSON (2::Int)
      req   = encode (Request method params reqId)
      len   = L.length req
  in L.take (len - 2) req

multishot :: [(Text, Value)] -> IO (Either String [Value])
multishot reqs = do
  let reqs' = [ encode (Request method params (toJSON i))
              | i <- [1::Int ..]
              | (method, params) <- reqs
              ]
      merged = rechunk 10 (L.concat reqs')
  vs <- runConduit
          $ B.sourceLbs merged
         .| server
         .| A.sinkParserEither (many' json')
  return . join . fmap sequence . bimap A.errorMessage (map resultOf) $ vs

rechunk :: Int64 -> L.ByteString -> L.ByteString
rechunk n = L.concat . split
  where
    split bs =
      let (p, s) = L.splitAt n bs
      in if L.null s then [p] else p : split s

resultOf :: Value -> Either String Value
resultOf = parseEither (withObject "message" (.: "result"))


server :: ConduitT S.ByteString S.ByteString IO ()
server = serve (fromList [ method "sum" sum'
                         , method "cat" cat ])

sum' :: [Int] -> IO (Either MethodError Int)
sum' = return . Right . sum

cat :: [String] -> IO (Either MethodError String)
cat = return . Right . concat