packages feed

linnet-0.1.0.0: test/Instances.hs

{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns        #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeApplications      #-}
{-# LANGUAGE UndecidableInstances  #-}

module Instances
  ( TestException(..)
  , Foo(..)
  ) where

import           Control.Applicative                  (empty)
import           Control.Exception                    (Exception,
                                                       SomeException (..),
                                                       catch, toException)
import qualified Control.Monad.Catch                  as MC
import qualified Data.ByteString                      as B
import           Data.ByteString.Builder              (toLazyByteString)
import qualified Data.ByteString.Builder              as Builder
import qualified Data.ByteString.Char8                as C8
import           Data.ByteString.Conversion           (ToByteString (..))
import qualified Data.ByteString.Lazy                 as BL
import qualified Data.ByteString.Lazy.Char8           as C8L
import qualified Data.CaseInsensitive                 as CI
import           Data.Function                        ((&))
import           Data.List.NonEmpty                   (NonEmpty (..), toList)
import           Data.Maybe                           (listToMaybe)
import qualified Data.Text                            as T
import qualified Data.Text.Encoding                   as TE
import           GHC.IO                               (unsafePerformIO)
import           Linnet                               (Decode (..), Encode (..),
                                                       TextHtml, TextPlain)
import           Linnet.Endpoint
import           Linnet.Endpoints.Entity
import           Linnet.Errors
import           Linnet.Input
import           Linnet.Output
import qualified Network.HTTP.Types                   as HTTP
import           Network.Wai                          (Request, defaultRequest,
                                                       pathInfo, queryString,
                                                       requestBody,
                                                       requestHeaders,
                                                       requestMethod,
                                                       strictRequestBody)
import           Network.Wai.Internal                 (Response (..))
import           Test.QuickCheck                      (Arbitrary (..),
                                                       CoArbitrary (..), Gen,
                                                       NonEmptyList, choose,
                                                       elements, getNonEmpty,
                                                       listOf, oneof, sample',
                                                       suchThat, vectorOf)
import           Test.QuickCheck.Instances.ByteString
import           Test.QuickCheck.Instances.Text
import           Util

genNonEmptyTuple :: Gen (B.ByteString, B.ByteString)
genNonEmptyTuple = do
  t1 <- suchThat arbitrary (/= B.empty)
  t2 <- suchThat arbitrary (/= B.empty)
  return (t1, t2)

genHeader :: Gen (CI.CI B.ByteString, B.ByteString)
genHeader = do
  (t1, t2) <- genNonEmptyTuple
  return (CI.mk t1, t2)

genParam :: Gen (B.ByteString, Maybe B.ByteString)
genParam = do
  (t1, t2) <- genNonEmptyTuple
  t2' <- elements [Just t2, Nothing]
  return (t1, t2')

genCookie :: Gen (CI.CI B.ByteString, B.ByteString)
genCookie = do
  kvs <- listOf genNonEmptyTuple
  let values = map (\t -> fst t `B.append` "=" `B.append` snd t) kvs
  return (CI.mk ("Cookie" :: B.ByteString), C8.intercalate ";" values)

genPath :: Gen [T.Text]
genPath = do
  size <- choose (0, 20)
  vectorOf size $ oneof [suchThat arbitrary (/= T.empty), T.pack . show <$> arbitrary @Int, elements ["true", "false"]]

genMethod :: Gen B.ByteString
genMethod =
  elements
    [ HTTP.methodGet
    , HTTP.methodPost
    , HTTP.methodHead
    , HTTP.methodPut
    , HTTP.methodDelete
    , HTTP.methodTrace
    , HTTP.methodConnect
    , HTTP.methodOptions
    , HTTP.methodPatch
    ]

genRequest :: Gen Request
genRequest = do
  path <- genPath
  cookie <- genCookie
  headers <- listOf genHeader
  params <- listOf genParam
  method <- genMethod
  return
    defaultRequest
      { requestMethod = method
      , queryString = params
      , requestBody = pure mempty
      , pathInfo = path
      , requestHeaders = cookie : headers
      }

genInput :: Gen Input
genInput = do
  req <- genRequest
  return Input {reminder = pathInfo req, request = req}

genStatus :: Gen HTTP.Status
genStatus =
  elements
    [ HTTP.continue100
    , HTTP.switchingProtocols101
    , HTTP.ok200
    , HTTP.created201
    , HTTP.accepted202
    , HTTP.nonAuthoritative203
    , HTTP.noContent204
    , HTTP.resetContent205
    , HTTP.partialContent206
    , HTTP.multipleChoices300
    , HTTP.movedPermanently301
    , HTTP.found302
    , HTTP.seeOther303
    , HTTP.notModified304
    , HTTP.useProxy305
    , HTTP.temporaryRedirect307
    , HTTP.permanentRedirect308
    , HTTP.badRequest400
    , HTTP.unauthorized401
    , HTTP.paymentRequired402
    , HTTP.forbidden403
    , HTTP.notFound404
    , HTTP.methodNotAllowed405
    , HTTP.notAcceptable406
    , HTTP.proxyAuthenticationRequired407
    , HTTP.requestTimeout408
    , HTTP.conflict409
    , HTTP.gone410
    , HTTP.lengthRequired411
    , HTTP.preconditionFailed412
    , HTTP.requestEntityTooLarge413
    , HTTP.requestURITooLong414
    , HTTP.unsupportedMediaType415
    , HTTP.requestedRangeNotSatisfiable416
    , HTTP.expectationFailed417
    , HTTP.imATeapot418
    , HTTP.unprocessableEntity422
    , HTTP.preconditionRequired428
    , HTTP.tooManyRequests429
    , HTTP.requestHeaderFieldsTooLarge431
    , HTTP.internalServerError500
    , HTTP.notImplemented501
    , HTTP.badGateway502
    , HTTP.serviceUnavailable503
    , HTTP.gatewayTimeout504
    , HTTP.networkAuthenticationRequired511
    , HTTP.httpVersionNotSupported505
    ]

genOutputMeta :: Gen (HTTP.Status, [(CI.CI B.ByteString, B.ByteString)])
genOutputMeta = do
  status <- genStatus
  headers <- listOf genHeader
  cookie <- genCookie
  return (status, cookie : headers)

genEmptyOutput :: Gen (Output a)
genEmptyOutput = do
  (status, headers) <- genOutputMeta
  return $ Output {outputStatus = status, outputHeaders = headers, outputPayload = NoPayload}

genFailureOutput :: Gen (Output a)
genFailureOutput = do
  (status, headers) <- genOutputMeta
  str <- arbitrary
  return $ Output {outputStatus = status, outputHeaders = headers, outputPayload = ErrorPayload $ TestException str}

genPayloadOutput :: (Arbitrary a) => Gen (Output a)
genPayloadOutput = do
  (status, headers) <- genOutputMeta
  a <- arbitrary
  return $ Output {outputStatus = status, outputHeaders = headers, outputPayload = Payload a}

genOutput :: (Arbitrary a) => Gen (Output a)
genOutput = oneof [genPayloadOutput, genEmptyOutput, genFailureOutput]

genEmptyEndpoint :: (MC.MonadCatch m) => Gen (Endpoint m a)
genEmptyEndpoint = pure empty

genConstEndpoint :: (MC.MonadCatch m, Arbitrary a) => Gen (Endpoint m a)
genConstEndpoint = pure <$> arbitrary

genErrorEndpoint :: (MC.MonadThrow m) => Gen (Endpoint m a)
genErrorEndpoint = lift . MC.throwM . TestException <$> arbitrary

genEndpoint ::
     forall m a. (Arbitrary (Input -> Output a), Monad m)
  => Gen (Endpoint m a)
genEndpoint = do
  f <- arbitrary @(Input -> Output a)
  return $
    Endpoint
      { runEndpoint = \input -> Matched {matchedReminder = input, matchedOutput = pure $ (f input)}
      , toString = "arbitrary"
      }

genEntity :: Gen Entity
genEntity = oneof [genParamEntity, genHeaderEntity, genCookieEntity, genBodyEntity]
  where
    genParamEntity = Param <$> arbitrary
    genHeaderEntity = Header <$> arbitrary
    genCookieEntity = Cookie <$> arbitrary
    genBodyEntity = pure Body

genLinnetError :: Gen LinnetError
genLinnetError = oneof [genDecodeError, genMissingEntity, genEntityNotParsed]
  where
    genDecodeError = DecodeError <$> arbitrary
    genMissingEntity = MissingEntity <$> genEntity
    genEntityNotParsed = EntityNotParsed <$> genEntity <*> genLinnetError
    genLinnetErrors =
      LinnetErrors <$> do
        h <- genLinnetError
        t <- listOf genLinnetError
        return $ h :| t

newtype TestException =
  TestException String
  deriving (Show, Eq)

instance Exception TestException

instance Arbitrary Request where
  arbitrary = genRequest

instance Arbitrary Entity where
  arbitrary = genEntity

instance Arbitrary Input where
  arbitrary = genInput

instance CoArbitrary (CI.CI B.ByteString) where
  coarbitrary ci = coarbitrary (CI.original ci)

instance CoArbitrary Input where
  coarbitrary input gen =
    coarbitrary (reminder input) gen & coarbitrary ((requestMethod . request) input) &
    coarbitrary ((requestHeaders . request) input) &
    coarbitrary ((queryString . request) input)

instance Arbitrary LinnetError where
  arbitrary = genLinnetError

instance Arbitrary a => Arbitrary (Output a) where
  arbitrary = genOutput

instance (Arbitrary a, MC.MonadCatch m) => Arbitrary (Endpoint m a) where
  arbitrary = oneof [genEndpoint, genErrorEndpoint, genConstEndpoint, genEmptyEndpoint]

instance (Eq a) => Eq (Payload a) where
  (==) (Payload a) (Payload b) = a == b
  (==) NoPayload NoPayload = True
  (==) (ErrorPayload ea) (ErrorPayload eb) = show (toException ea) == show (toException eb)
  (==) _ _ = False

instance (Eq a) => Eq (Output a) where
  (==) (Output sa pa ha) (Output sb pb hb) = sa == sb && pa == pb && ha == hb

instance Eq Input where
  (==) i i' = reminder i == reminder i' && request i == request i'

instance Eq SomeException where
  (==) e e' = show e == show e'

instance Eq Request where
  (==) r r' = show r == show r'

instance Eq (m (Output a)) => Eq (EndpointResult m a) where
  (==) (Matched i m) (Matched i' m') = i == i' && m == m'
  (==) NotMatched NotMatched         = True
  (==) _ _                           = False

instance Eq (m (Output a)) => Eq (Endpoint m a) where
  (==) ea eb = runEndpoint ea input == runEndpoint eb input
    where
      input = head . unsafePerformIO . sample' $ genInput

instance Decode TextPlain T.Text where
  decode = Right . TE.decodeUtf8 . BL.toStrict

instance Eq a => Eq (IO a) where
  (==) i i' = unsafePerformIO (tryAll i) == unsafePerformIO (tryAll i')
    where
      tryAll m = MC.catchAll (fmap Right m) (pure . Left)

instance Eq Response where
  (==) (ResponseBuilder s hs b) (ResponseBuilder s' hs' b') =
    s == s && hs == hs' && toLazyByteString b == toLazyByteString b'

newtype Foo =
  Foo
    { x :: String
    }
  deriving (Show, Read, Eq)

instance Arbitrary Foo where
  arbitrary = Foo <$> arbitrary

instance Decode TextPlain Foo where
  decode bs =
    case (reads . C8.unpack . BL.toStrict) bs of
      [(foo, _)] -> Right foo
      _          -> Left (DecodeError "Couldn't parse Foo")

instance Encode ct SomeException where
  encode = C8L.pack . show

instance Encode TextHtml T.Text where
  encode = BL.fromStrict . TE.encodeUtf8