packages feed

linnet-conduit-0.3.0.0: test/ConduitSpec.hs

{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeApplications      #-}
{-# LANGUAGE TypeSynonymInstances  #-}

module ConduitSpec where

import           Conduit                   (ConduitT, runConduit, yield, (.|))
import           Control.Monad.IO.Class    (liftIO)
import qualified Data.ByteString           as BS
import qualified Data.ByteString.Builder   as Builder
import           Data.ByteString.Char8     (unpack)
import qualified Data.ByteString.Lazy      as BL
import           Data.Conduit.Combinators  (sinkList)
import           Data.IORef                (atomicModifyIORef, modifyIORef,
                                            newIORef, readIORef)
import           Data.List                 (intersperse)
import           Linnet
import           Linnet.Conduit
import           Linnet.Endpoint           (EndpointResult (..), isMatched,
                                            runEndpoint)
import           Linnet.Input
import           Linnet.ToResponse
import           Network.HTTP.Types.Status (ok200)
import           Network.Wai               (RequestBodyLength (..),
                                            defaultRequest, requestBody,
                                            requestBodyLength, responseStream,
                                            responseToStream)
import           Test.Hspec
import           Test.QuickCheck           (property)
import           Test.QuickCheck.Instances
import           Test.QuickCheck.Monadic   (assert, monadicIO)

spec :: Spec
spec =
  describe "Conduit streaming" $ do
    describe "streamBody" $ do
      it "should match only Chunked requests" $ do
        let streamed = inputFromRequest $ defaultRequest {requestBodyLength = ChunkedBody}
        let fixed = inputFromRequest defaultRequest
        let endpoint = streamBody @IO
        isMatched (runEndpoint endpoint streamed) `shouldBe` True
        isMatched (runEndpoint endpoint fixed) `shouldBe` False
      it "streams request body as conduit stream" $
        property $ \(bytes :: [BS.ByteString]) ->
          let nonEmpty = filter (/= mempty) bytes
              getChunk :: [BS.ByteString] -> ([BS.ByteString], BS.ByteString)
              getChunk []    = ([], BS.empty)
              getChunk (h:t) = (t, h)
           in monadicIO $ do
                ref <- liftIO $ newIORef nonEmpty
                let streamed =
                      inputFromRequest $
                      defaultRequest {requestBodyLength = ChunkedBody, requestBody = atomicModifyIORef ref getChunk}
                let endpoint = streamBody @IO ~> (\stream -> ok <$> runConduit (stream .| sinkList))
                let (Matched _ mOut) = runEndpoint endpoint streamed
                output <- liftIO mOut
                assert $ output == ok (map BL.fromStrict nonEmpty)
    describe "ToResponse conduit" $ do
      it "should create chunked response" $
        property $ \(bytes :: [BS.ByteString]) ->
          let stream :: [BS.ByteString] -> ConduitT () BS.ByteString IO ()
              stream = foldr ((>>) . yield) (return ())
           in monadicIO $ do
                final <- liftIO $ newIORef ([] @BS.ByteString)
                buffer <- liftIO $ newIORef ([] @BS.ByteString)
                let response = toResponse @TextPlain ok200 [] $ stream bytes
                let (_, _, responseBody) = responseToStream response
                let write builder = modifyIORef buffer (++ [BL.toStrict $ Builder.toLazyByteString builder])
                let flush = do
                      buf <- readIORef buffer
                      _ <- modifyIORef final (++ buf)
                      _ <- modifyIORef buffer (const [])
                      return ()
                result <- liftIO $ responseBody (\fn -> fn write flush >> readIORef final)
                assert (result == bytes)
      it "should intersperse newline symbol in JSON stream" $
        property $ \(bytes :: [BS.ByteString]) ->
          let stream :: [BS.ByteString] -> ConduitT () BS.ByteString IO ()
              stream = foldr ((>>) . yield) (return ())
           in monadicIO $ do
                final <- liftIO $ newIORef ([] @BS.ByteString)
                buffer <- liftIO $ newIORef ([] @BS.ByteString)
                let response = toResponse @ApplicationJson ok200 [] $ stream bytes
                let (_, _, responseBody) = responseToStream response
                let write builder = modifyIORef buffer (++ [BL.toStrict $ Builder.toLazyByteString builder])
                let flush = do
                      buf <- readIORef buffer
                      _ <- modifyIORef final (++ buf)
                      _ <- modifyIORef buffer (const [])
                      return ()
                result <- liftIO $ responseBody (\fn -> fn write flush >> readIORef final)
                assert
                  (result ==
                   if null bytes
                     then []
                     else intersperse "\n" bytes ++ ["\n"])

instance Encode ApplicationJson BS.ByteString where
  encode = BL.fromStrict