2015-04-20 19:52:29 +02:00
|
|
|
|
{-# LANGUAGE CPP #-}
|
2015-02-20 11:13:10 +01:00
|
|
|
|
{-# LANGUAGE DataKinds #-}
|
|
|
|
|
{-# LANGUAGE DeriveGeneric #-}
|
|
|
|
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2016-04-16 17:13:58 +02:00
|
|
|
|
{-# LANGUAGE PackageImports #-}
|
2016-01-20 00:40:49 +01:00
|
|
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
2015-09-10 08:49:19 +02:00
|
|
|
|
{-# LANGUAGE PolyKinds #-}
|
2015-02-20 01:07:36 +01:00
|
|
|
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
|
|
|
|
module Servant.API.ContentTypesSpec where
|
|
|
|
|
|
2016-05-11 10:51:39 +02:00
|
|
|
|
import Prelude ()
|
|
|
|
|
import Prelude.Compat
|
|
|
|
|
|
2015-02-23 06:54:10 +01:00
|
|
|
|
import Control.Arrow
|
2016-01-20 00:40:49 +01:00
|
|
|
|
import Control.Monad (when)
|
|
|
|
|
import Control.Monad.Except (runExceptT)
|
|
|
|
|
import Control.Monad.Trans (liftIO)
|
2015-02-20 01:07:36 +01:00
|
|
|
|
import Data.Aeson
|
2015-08-17 23:56:29 +02:00
|
|
|
|
import Data.ByteString.Char8 (ByteString, append, pack)
|
|
|
|
|
import qualified Data.ByteString.Lazy as BSL
|
2015-05-16 04:40:26 +02:00
|
|
|
|
import Data.Either
|
2015-08-17 23:56:29 +02:00
|
|
|
|
import Data.Function (on)
|
|
|
|
|
import Data.List (maximumBy)
|
|
|
|
|
import Data.Maybe (fromJust, isJust, isNothing)
|
2015-02-20 01:07:36 +01:00
|
|
|
|
import Data.Proxy
|
2015-08-17 23:56:29 +02:00
|
|
|
|
import Data.String (IsString (..))
|
|
|
|
|
import Data.String.Conversions (cs)
|
|
|
|
|
import qualified Data.Text as TextS
|
|
|
|
|
import qualified Data.Text.Lazy as TextL
|
2015-02-20 01:07:36 +01:00
|
|
|
|
import GHC.Generics
|
2015-08-17 23:56:29 +02:00
|
|
|
|
import Network.URL (exportParams, importParams)
|
2015-02-20 01:07:36 +01:00
|
|
|
|
import Test.Hspec
|
|
|
|
|
import Test.QuickCheck
|
2016-01-20 00:40:49 +01:00
|
|
|
|
import Test.QuickCheck.Monadic
|
2016-04-16 17:13:58 +02:00
|
|
|
|
import "quickcheck-instances" Test.QuickCheck.Instances ()
|
2015-02-20 01:07:36 +01:00
|
|
|
|
|
|
|
|
|
import Servant.API.ContentTypes
|
|
|
|
|
|
2016-01-20 00:40:49 +01:00
|
|
|
|
shouldUnrenderTo :: (MimeUnrender ct a, Eq a, Show a) => (Proxy ct, BSL.ByteString) -> a -> IO ()
|
|
|
|
|
shouldUnrenderTo (p, bs) x = do
|
|
|
|
|
res <- runExceptT (mimeUnrender p bs)
|
|
|
|
|
res `shouldBe` Right x
|
|
|
|
|
|
|
|
|
|
shouldNotUnrenderTo :: forall ct a. (MimeUnrender ct a, Show a) =>
|
|
|
|
|
(Proxy ct, BSL.ByteString) -> Proxy a -> IO ()
|
|
|
|
|
shouldNotUnrenderTo (p, bs) _ = do
|
|
|
|
|
res <- runExceptT (mimeUnrender p bs)
|
|
|
|
|
res `shouldSatisfy` (isLeft :: Either e a -> Bool)
|
|
|
|
|
|
|
|
|
|
shouldHandleCTypeH :: (AllCTUnrender cts a, Eq a, Show a) =>
|
|
|
|
|
(Proxy cts, BSL.ByteString, BSL.ByteString) -> a -> IO ()
|
|
|
|
|
shouldHandleCTypeH (p, ct, bs) x = do
|
|
|
|
|
res <- traverse runExceptT (handleCTypeH p ct bs)
|
|
|
|
|
res `shouldBe` Just (Right x)
|
|
|
|
|
|
|
|
|
|
shouldNotFindCTypeH :: forall cts a. (AllCTUnrender cts a, Eq a, Show a) =>
|
|
|
|
|
(Proxy cts, BSL.ByteString, BSL.ByteString) -> Proxy a -> IO ()
|
|
|
|
|
shouldNotFindCTypeH (p, ct, bs) _ = do
|
|
|
|
|
res <- traverse runExceptT (handleCTypeH p ct bs)
|
|
|
|
|
res `shouldBe` (Nothing :: Maybe (Either String a))
|
|
|
|
|
|
|
|
|
|
shouldHandleCTypeHSatisfy :: forall cts a. (AllCTUnrender cts a, Show a) =>
|
|
|
|
|
(Proxy cts, BSL.ByteString, BSL.ByteString) -> (Maybe (Either String a) -> Bool) -> IO ()
|
|
|
|
|
shouldHandleCTypeHSatisfy (p, ct, bs) f = do
|
|
|
|
|
res <- traverse runExceptT (handleCTypeH p ct bs)
|
|
|
|
|
res `shouldSatisfy` f
|
|
|
|
|
|
2015-02-20 01:07:36 +01:00
|
|
|
|
spec :: Spec
|
|
|
|
|
spec = describe "Servant.API.ContentTypes" $ do
|
|
|
|
|
|
2015-09-10 08:49:19 +02:00
|
|
|
|
describe "handleAcceptH" $ do
|
|
|
|
|
let p = Proxy :: Proxy '[PlainText]
|
|
|
|
|
|
|
|
|
|
it "matches any charset if none were provided" $ do
|
|
|
|
|
let without = handleAcceptH p (AcceptHeader "text/plain")
|
|
|
|
|
with = handleAcceptH p (AcceptHeader "text/plain;charset=utf-8")
|
|
|
|
|
wisdom = "ubi sub ubi" :: String
|
|
|
|
|
without wisdom `shouldBe` with wisdom
|
|
|
|
|
|
|
|
|
|
it "does not match non utf-8 charsets" $ do
|
|
|
|
|
let badCharset = handleAcceptH p (AcceptHeader "text/plain;charset=whoknows")
|
|
|
|
|
s = "cheese" :: String
|
|
|
|
|
badCharset s `shouldBe` Nothing
|
|
|
|
|
|
2015-02-20 01:07:36 +01:00
|
|
|
|
describe "The JSON Content-Type type" $ do
|
2015-05-16 03:11:38 +02:00
|
|
|
|
let p = Proxy :: Proxy JSON
|
|
|
|
|
|
|
|
|
|
it "handles whitespace at end of input" $ do
|
2016-01-20 00:40:49 +01:00
|
|
|
|
(p, "[1] ") `shouldUnrenderTo` [1 :: Int]
|
2015-05-16 03:11:38 +02:00
|
|
|
|
|
2015-05-16 04:40:26 +02:00
|
|
|
|
it "handles whitespace at beginning of input" $ do
|
2016-01-20 00:40:49 +01:00
|
|
|
|
(p, " [1]") `shouldUnrenderTo` [1 :: Int]
|
2015-05-16 04:40:26 +02:00
|
|
|
|
|
2015-05-16 03:11:38 +02:00
|
|
|
|
it "does not like junk at end of input" $ do
|
2016-01-20 00:40:49 +01:00
|
|
|
|
(p, "[1] this probably shouldn't work")
|
|
|
|
|
`shouldNotUnrenderTo` (Proxy :: Proxy [Int])
|
2015-02-20 01:07:36 +01:00
|
|
|
|
|
2015-04-13 15:12:33 +02:00
|
|
|
|
it "has mimeUnrender reverse mimeRender for valid top-level json ([Int]) " $ do
|
2016-01-20 00:40:49 +01:00
|
|
|
|
property $ \x -> monadicIO $ do
|
|
|
|
|
res <- liftIO $ runExceptT (mimeUnrender p (mimeRender p x))
|
|
|
|
|
assert (res == Right (x::[Int]))
|
2015-02-20 01:07:36 +01:00
|
|
|
|
|
2015-04-13 15:12:33 +02:00
|
|
|
|
it "has mimeUnrender reverse mimeRender for valid top-level json " $ do
|
2016-01-20 00:40:49 +01:00
|
|
|
|
property $ \x -> monadicIO $ do
|
|
|
|
|
res <- liftIO $ runExceptT (mimeUnrender p (mimeRender p x))
|
|
|
|
|
assert (res == Right (x::[SomeData]))
|
2015-02-20 01:07:36 +01:00
|
|
|
|
|
2015-02-20 21:44:28 +01:00
|
|
|
|
describe "The FormUrlEncoded Content-Type type" $ do
|
2015-05-16 03:11:38 +02:00
|
|
|
|
let p = Proxy :: Proxy FormUrlEncoded
|
2015-02-25 12:48:15 +01:00
|
|
|
|
|
2015-04-13 15:12:33 +02:00
|
|
|
|
it "has mimeUnrender reverse mimeRender" $ do
|
2016-01-20 00:40:49 +01:00
|
|
|
|
property $ \x -> monadicIO $ when (mempty `notElem` x) $ do
|
|
|
|
|
res <- liftIO . runExceptT $ mimeUnrender p (mimeRender p x)
|
|
|
|
|
assert (res == Right (x::[(TextS.Text,TextS.Text)]))
|
2015-02-20 21:44:28 +01:00
|
|
|
|
|
2015-04-13 15:12:33 +02:00
|
|
|
|
it "has mimeUnrender reverse exportParams (Network.URL)" $ do
|
2016-01-20 00:40:49 +01:00
|
|
|
|
property $ \x -> monadicIO $ when (mempty `notElem` x) $ do
|
|
|
|
|
res <- liftIO . runExceptT $ mimeUnrender p . cs . exportParams . map (cs *** cs) $ x
|
|
|
|
|
assert (res == Right (x::[(TextS.Text,TextS.Text)]))
|
2015-02-23 06:54:10 +01:00
|
|
|
|
|
2015-04-13 15:12:33 +02:00
|
|
|
|
it "has importParams (Network.URL) reverse mimeRender" $ do
|
2015-08-17 23:50:42 +02:00
|
|
|
|
property $ \x -> mempty `notElem` x
|
2015-04-13 15:12:33 +02:00
|
|
|
|
==> (fmap (map (cs *** cs)) . importParams . cs . mimeRender p $ x) == Just (x::[(TextS.Text,TextS.Text)])
|
2015-02-23 06:54:10 +01:00
|
|
|
|
|
2015-02-20 01:07:36 +01:00
|
|
|
|
describe "The PlainText Content-Type type" $ do
|
2015-05-16 03:11:38 +02:00
|
|
|
|
let p = Proxy :: Proxy PlainText
|
2015-02-20 01:07:36 +01:00
|
|
|
|
|
2015-04-13 15:12:33 +02:00
|
|
|
|
it "has mimeUnrender reverse mimeRender (lazy Text)" $ do
|
2016-01-20 00:40:49 +01:00
|
|
|
|
property $ \x -> monadicIO $ do
|
|
|
|
|
res <- liftIO . runExceptT $ mimeUnrender p (mimeRender p x)
|
|
|
|
|
assert (res == Right (x::TextL.Text))
|
2015-02-20 01:07:36 +01:00
|
|
|
|
|
2015-04-13 15:12:33 +02:00
|
|
|
|
it "has mimeUnrender reverse mimeRender (strict Text)" $ do
|
2016-01-20 00:40:49 +01:00
|
|
|
|
property $ \x -> monadicIO $ do
|
|
|
|
|
res <- liftIO . runExceptT $ mimeUnrender p (mimeRender p x)
|
|
|
|
|
assert (res == Right (x::TextS.Text))
|
2015-02-20 01:07:36 +01:00
|
|
|
|
|
2015-02-20 11:13:10 +01:00
|
|
|
|
describe "The OctetStream Content-Type type" $ do
|
2015-05-16 03:11:38 +02:00
|
|
|
|
let p = Proxy :: Proxy OctetStream
|
2015-02-20 11:13:10 +01:00
|
|
|
|
|
|
|
|
|
it "is id (Lazy ByteString)" $ do
|
2016-01-20 00:40:49 +01:00
|
|
|
|
property $ \x -> monadicIO $ do
|
|
|
|
|
assert (mimeRender p x == (x :: BSL.ByteString))
|
|
|
|
|
res <- liftIO . runExceptT $ mimeUnrender p x
|
|
|
|
|
assert (res == Right x)
|
2015-02-20 11:13:10 +01:00
|
|
|
|
|
|
|
|
|
it "is fromStrict/toStrict (Strict ByteString)" $ do
|
2016-01-20 00:40:49 +01:00
|
|
|
|
property $ \x -> monadicIO $ do
|
|
|
|
|
assert (mimeRender p x == BSL.fromStrict (x :: ByteString))
|
|
|
|
|
res <- liftIO . runExceptT $ mimeUnrender p (BSL.fromStrict x)
|
|
|
|
|
assert (res == Right x)
|
2015-02-20 11:13:10 +01:00
|
|
|
|
|
|
|
|
|
describe "handleAcceptH" $ do
|
|
|
|
|
|
2015-02-20 15:05:24 +01:00
|
|
|
|
it "returns Nothing if the 'Accept' header doesn't match" $ do
|
|
|
|
|
handleAcceptH (Proxy :: Proxy '[JSON]) "text/plain" (3 :: Int)
|
|
|
|
|
`shouldSatisfy` isNothing
|
|
|
|
|
|
2015-02-20 11:13:10 +01:00
|
|
|
|
it "returns Just if the 'Accept' header matches" $ do
|
|
|
|
|
handleAcceptH (Proxy :: Proxy '[JSON]) "*/*" (3 :: Int)
|
|
|
|
|
`shouldSatisfy` isJust
|
|
|
|
|
handleAcceptH (Proxy :: Proxy '[PlainText, JSON]) "application/json" (3 :: Int)
|
|
|
|
|
`shouldSatisfy` isJust
|
|
|
|
|
handleAcceptH (Proxy :: Proxy '[PlainText, JSON, OctetStream])
|
|
|
|
|
"application/octet-stream" ("content" :: ByteString)
|
|
|
|
|
`shouldSatisfy` isJust
|
|
|
|
|
|
|
|
|
|
it "returns the Content-Type as the first element of the tuple" $ do
|
|
|
|
|
handleAcceptH (Proxy :: Proxy '[JSON]) "*/*" (3 :: Int)
|
2015-02-20 15:05:24 +01:00
|
|
|
|
`shouldSatisfy` ((== "application/json") . fst . fromJust)
|
2015-02-20 11:13:10 +01:00
|
|
|
|
handleAcceptH (Proxy :: Proxy '[PlainText, JSON]) "application/json" (3 :: Int)
|
2015-02-20 15:05:24 +01:00
|
|
|
|
`shouldSatisfy` ((== "application/json") . fst . fromJust)
|
2015-02-20 11:13:10 +01:00
|
|
|
|
handleAcceptH (Proxy :: Proxy '[PlainText, JSON, OctetStream])
|
|
|
|
|
"application/octet-stream" ("content" :: ByteString)
|
|
|
|
|
`shouldSatisfy` ((== "application/octet-stream") . fst . fromJust)
|
|
|
|
|
|
|
|
|
|
it "returns the appropriately serialized representation" $ do
|
|
|
|
|
property $ \x -> handleAcceptH (Proxy :: Proxy '[JSON]) "*/*" (x :: SomeData)
|
2015-02-20 15:05:24 +01:00
|
|
|
|
== Just ("application/json", encode x)
|
|
|
|
|
|
|
|
|
|
it "respects the Accept spec ordering" $ do
|
|
|
|
|
let highest a b c = maximumBy (compare `on` snd)
|
|
|
|
|
[ ("application/octet-stream", a)
|
|
|
|
|
, ("application/json", b)
|
|
|
|
|
, ("text/plain;charset=utf-8", c)
|
|
|
|
|
]
|
|
|
|
|
let acceptH a b c = addToAccept (Proxy :: Proxy OctetStream) a $
|
|
|
|
|
addToAccept (Proxy :: Proxy JSON) b $
|
|
|
|
|
addToAccept (Proxy :: Proxy PlainText ) c ""
|
|
|
|
|
let val a b c i = handleAcceptH (Proxy :: Proxy '[OctetStream, JSON, PlainText])
|
2015-02-20 11:13:10 +01:00
|
|
|
|
(acceptH a b c) (i :: Int)
|
2015-02-20 15:05:24 +01:00
|
|
|
|
property $ \a b c i -> fst (fromJust $ val a b c i) == fst (highest a b c)
|
|
|
|
|
|
|
|
|
|
describe "handleCTypeH" $ do
|
|
|
|
|
|
|
|
|
|
it "returns Nothing if the 'Content-Type' header doesn't match" $ do
|
2016-01-20 00:40:49 +01:00
|
|
|
|
(Proxy :: Proxy '[JSON], "text/plain", "𝓽𝓱𝓮 𝓽𝓲𝓶𝓮 𝓱𝓪𝓼 𝓬𝓸𝓶𝓮, 𝓽𝓱𝓮 𝔀𝓪𝓵𝓻𝓾𝓼 𝓼𝓪𝓲𝓭 ")
|
|
|
|
|
`shouldNotFindCTypeH` (Proxy :: Proxy Value)
|
2015-02-20 15:05:24 +01:00
|
|
|
|
|
|
|
|
|
context "the 'Content-Type' header matches" $ do
|
|
|
|
|
it "returns Just if the parameter matches" $ do
|
2016-01-20 00:40:49 +01:00
|
|
|
|
(Proxy :: Proxy '[JSON], "application/json", "𝕥𝕠 𝕥𝕒𝕝𝕜 𝕠𝕗 𝕞𝕒𝕟𝕪 𝕥𝕙𝕚𝕟𝕘𝕤 ")
|
|
|
|
|
`shouldHandleCTypeHSatisfy` (isJust :: Maybe (Either String Value) -> Bool)
|
2015-02-20 15:05:24 +01:00
|
|
|
|
|
|
|
|
|
it "returns Just if there is no parameter" $ do
|
2016-01-20 00:40:49 +01:00
|
|
|
|
(Proxy :: Proxy '[JSON], "application/json", "𝕥𝕠 𝕥𝕒𝕝𝕜 𝕠𝕗 𝕞𝕒𝕟𝕪 𝕥𝕙𝕚𝕟𝕘𝕤 ")
|
|
|
|
|
`shouldHandleCTypeHSatisfy` (isJust :: Maybe (Either String Value) -> Bool)
|
2015-02-20 15:05:24 +01:00
|
|
|
|
|
|
|
|
|
it "returns Just Left if the decoding fails" $ do
|
|
|
|
|
let isJustLeft :: Maybe (Either String Value) -> Bool
|
|
|
|
|
isJustLeft (Just (Left _)) = True
|
|
|
|
|
isJustLeft _ = False
|
2016-01-20 00:40:49 +01:00
|
|
|
|
(Proxy :: Proxy '[JSON], "application/json", "𝕺𝖋 𝖘𝖍𝖔𝖊𝖘--𝖆𝖓𝖉 𝖘𝖍𝖎𝖕𝖘--𝖆𝖓𝖉 𝖘𝖊𝖆𝖑𝖎𝖓𝖌-𝖜𝖆𝖝-- ")
|
|
|
|
|
`shouldHandleCTypeHSatisfy` isJustLeft
|
2015-02-20 15:05:24 +01:00
|
|
|
|
|
2015-02-20 15:22:26 +01:00
|
|
|
|
it "returns Just (Right val) if the decoding succeeds" $ do
|
|
|
|
|
let val = SomeData "Of cabbages--and kings" 12
|
2016-01-20 00:40:49 +01:00
|
|
|
|
(Proxy :: Proxy '[JSON], "application/json", encode val)
|
|
|
|
|
`shouldHandleCTypeH` val
|
2015-02-20 15:22:26 +01:00
|
|
|
|
|
2015-06-23 06:55:13 +02:00
|
|
|
|
#if MIN_VERSION_aeson(0,9,0)
|
|
|
|
|
-- aeson >= 0.9 decodes top-level strings
|
2015-02-25 12:48:15 +01:00
|
|
|
|
describe "eitherDecodeLenient" $ do
|
|
|
|
|
|
|
|
|
|
it "parses top-level strings" $ do
|
|
|
|
|
let toMaybe = either (const Nothing) Just
|
|
|
|
|
-- The Left messages differ, so convert to Maybe
|
|
|
|
|
property $ \x -> toMaybe (eitherDecodeLenient x)
|
2015-06-12 15:45:51 +02:00
|
|
|
|
`shouldBe` (decode x :: Maybe String)
|
|
|
|
|
#endif
|
2015-02-25 12:48:15 +01:00
|
|
|
|
|
2015-02-20 01:07:36 +01:00
|
|
|
|
|
|
|
|
|
data SomeData = SomeData { record1 :: String, record2 :: Int }
|
|
|
|
|
deriving (Generic, Eq, Show)
|
|
|
|
|
|
2015-02-20 11:13:10 +01:00
|
|
|
|
newtype ZeroToOne = ZeroToOne Float
|
|
|
|
|
deriving (Eq, Show, Ord)
|
|
|
|
|
|
2015-02-20 01:07:36 +01:00
|
|
|
|
instance FromJSON SomeData
|
2015-02-20 11:13:10 +01:00
|
|
|
|
|
2015-02-20 01:07:36 +01:00
|
|
|
|
instance ToJSON SomeData
|
2015-02-20 11:13:10 +01:00
|
|
|
|
|
2015-02-20 01:07:36 +01:00
|
|
|
|
instance Arbitrary SomeData where
|
|
|
|
|
arbitrary = SomeData <$> arbitrary <*> arbitrary
|
|
|
|
|
|
2015-02-20 11:13:10 +01:00
|
|
|
|
instance Arbitrary ZeroToOne where
|
|
|
|
|
arbitrary = ZeroToOne <$> elements [ x / 10 | x <- [1..10]]
|
|
|
|
|
|
|
|
|
|
instance MimeRender OctetStream Int where
|
2015-04-13 15:12:33 +02:00
|
|
|
|
mimeRender _ = cs . show
|
2015-02-20 11:13:10 +01:00
|
|
|
|
|
|
|
|
|
instance MimeRender PlainText Int where
|
2015-04-13 15:12:33 +02:00
|
|
|
|
mimeRender _ = cs . show
|
2015-02-20 11:13:10 +01:00
|
|
|
|
|
|
|
|
|
instance MimeRender PlainText ByteString where
|
2015-04-13 15:12:33 +02:00
|
|
|
|
mimeRender _ = cs
|
2015-02-20 11:13:10 +01:00
|
|
|
|
|
|
|
|
|
instance ToJSON ByteString where
|
|
|
|
|
toJSON x = object [ "val" .= x ]
|
|
|
|
|
|
|
|
|
|
instance IsString AcceptHeader where
|
|
|
|
|
fromString = AcceptHeader . fromString
|
|
|
|
|
|
|
|
|
|
addToAccept :: Accept a => Proxy a -> ZeroToOne -> AcceptHeader -> AcceptHeader
|
|
|
|
|
addToAccept p (ZeroToOne f) (AcceptHeader h) = AcceptHeader (cont h)
|
|
|
|
|
where new = cs (show $ contentType p) `append` "; q=" `append` pack (show f)
|
|
|
|
|
cont "" = new
|
|
|
|
|
cont old = old `append` ", " `append` new
|