{-# LANGUAGE DataKinds #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeOperators #-} import Data.ByteString as BS (ByteString) import Data.ByteString.Lazy as BSL (ByteString) import Data.List (intersperse) import Data.Monoid import Data.String.Conversions (cs) import Data.Text (Text) import Network.HTTP.Types.Header (HeaderName, hContentType) import Test.Tasty import Test.Tasty.Wai import Servant import Servant.Multipart main :: IO () main = defaultMain $ testGroup "servant-multipart" [ testGroup "strict handler with FromMultipart" [ testWai testApp "correct body" testBlogPostStrictHandler , testWai testApp "empty body" testBlogPostStrictHandlerEmptyBody , testWai testApp "partial body" testBlogPostStrictHandlerPartialBody ] , testGroup "lenient handler with FromMultipart" [ testWai testApp "correct body" testBlogPostLenientHandler , testWai testApp "partial body" testBlogPostLenientHandlerPartialBody ] , testGroup "strict handler with raw MultipartData" [ testWai testApp "correct body" testBlogPostRawHandler ] ] data BlogPost = BlogPost { title :: Text , body :: Text } instance FromMultipart Mem BlogPost where fromMultipart md = BlogPost <$> lookupInput "title" md <*> fmap (cs . fdPayload) (lookupFile "body" md) type TestAPI = "blogPostStrict" :> MultipartForm Mem BlogPost :> Post '[PlainText] Text :<|> "blogPostLenient" :> MultipartForm' '[Lenient] Mem BlogPost :> Post '[JSON] Bool :<|> "blogPostRaw" :> MultipartForm Mem (MultipartData Mem) :> Post '[PlainText] Text blogPostStrictHandler :: BlogPost -> Handler Text blogPostStrictHandler bp = return $ title bp <> "\n" <> body bp blogPostLenientHandler :: Either String BlogPost -> Handler Bool blogPostLenientHandler eitherBP = case eitherBP of Left _ -> return False Right _ -> return True blogPostRawHandler :: MultipartData Mem -> Handler Text blogPostRawHandler md = return $ mconcat $ intersperse " " $ map iName (inputs md) <> map fdInputName (files md) testApp :: Application testApp = serve @TestAPI Proxy $ blogPostStrictHandler :<|> blogPostLenientHandler :<|> blogPostRawHandler multipartHeaders :: [(HeaderName, BS.ByteString)] multipartHeaders = [(hContentType, "multipart/form-data; boundary=XX")] testBlogPostStrictHandler :: Session () testBlogPostStrictHandler = do res <- srequest $ buildRequestWithHeaders POST "/blogPostStrict" correctBody multipartHeaders assertStatus 200 res assertBody "Foo post\nFoo body\n" res testBlogPostStrictHandlerEmptyBody :: Session () testBlogPostStrictHandlerEmptyBody = do res <- srequest $ buildRequestWithHeaders POST "/blogPostStrict" "" multipartHeaders assertStatus 400 res assertBody "Could not decode multipart mime body: Field title not found" res testBlogPostStrictHandlerPartialBody :: Session () testBlogPostStrictHandlerPartialBody = do res <- srequest $ buildRequestWithHeaders POST "/blogPostStrict" partialBody multipartHeaders assertStatus 400 res assertBody "Could not decode multipart mime body: File body not found" res testBlogPostLenientHandler :: Session () testBlogPostLenientHandler = do res <- srequest $ buildRequestWithHeaders POST "/blogPostLenient" correctBody multipartHeaders assertStatus 200 res assertBody "true" res testBlogPostLenientHandlerPartialBody :: Session () testBlogPostLenientHandlerPartialBody = do res <- srequest $ buildRequestWithHeaders POST "/blogPostLenient" partialBody multipartHeaders assertStatus 200 res assertBody "false" res testBlogPostRawHandler :: Session () testBlogPostRawHandler = do res <- srequest $ buildRequestWithHeaders POST "/blogPostRaw" correctBody multipartHeaders assertStatus 200 res assertBody "title body" res correctBody :: BSL.ByteString correctBody = mconcat $ intersperse "\n" [ "--XX" , "Content-Disposition: form-data; name=\"title\"" , "" , "Foo post" , "--XX" , "Content-Disposition: form-data; name=\"body\"; filename=\"body.md\"" , "" , "Foo body" , "" , "--XX--" ] partialBody :: BSL.ByteString partialBody = mconcat $ intersperse "\n" [ "--XX" , "Content-Disposition: form-data; name=\"title\"" , "" , "Foo post" , "" , "--XX--" ]