packages feed

scotty-0.22: test/Test/Hspec/Wai/Extra.hs

-- | This should be in 'hspec-wai', PR pending as of Feb 2024 : https://github.com/hspec/hspec-wai/pull/77
--
-- NB the code below has been changed wrt PR 77 and works in the scotty test suite as well.

{-# language OverloadedStrings #-}
module Test.Hspec.Wai.Extra (postMultipartForm, FileMeta(..)) where

import qualified Data.Char as Char
import Data.List (intersperse)

import Data.ByteString (ByteString)
import qualified Data.ByteString.Builder as Builder
import qualified Data.ByteString.Lazy as LB

import Data.Word (Word8)

import Network.HTTP.Types (methodPost, hContentType)
import Network.Wai.Test (SResponse)

import Test.Hspec.Wai (request)
import Test.Hspec.Wai.Internal (WaiSession)

-- | @POST@ a @multipart/form-data@ form which might include files.
--
-- The @Content-Type@ is set to @multipart/form-data; boundary=<bd>@ where @bd@ is the part separator without the @--@ prefix.
postMultipartForm :: ByteString -- ^ path
                  -> ByteString -- ^ part separator without any dashes
                  -> [(FileMeta, ByteString, ByteString, ByteString)] -- ^ (file metadata, field MIME type, field name, field contents)
                  -> WaiSession st SResponse
postMultipartForm path sbs =
  request methodPost path [(hContentType, "multipart/form-data; boundary=" <> sbs)] . formMultipartQuery sbs

-- | Encode the body of a multipart form post
--
-- schema from : https://swagger.io/docs/specification/describing-request-body/multipart-requests/
formMultipartQuery :: ByteString -- ^ part separator without any dashes
                   -> [(FileMeta, ByteString, ByteString, ByteString)] -- ^ (file metadata, field MIME type, field name, field contents)
                   -> LB.ByteString
formMultipartQuery sbs = Builder.toLazyByteString . mconcat . intersperse newline . encodeAll
  where
    encodeAll fs = map encodeFile fs <> [sepEnd]
    encodeFile (fieldMeta, ty, n, payload) = mconcat $ [
      sep
      , newline
      , kv "Content-Disposition" ("form-data;" <> " name=" <> quoted n <> encodeMPField fieldMeta)
      , newline
      , kv "Content-Type" (Builder.byteString ty)
      , newline, newline
      , Builder.byteString payload
      ]
    sep = Builder.byteString ("--" <> sbs)
    sepEnd = Builder.byteString ("--" <> sbs <> "--")
    encodeMPField FMFormField = mempty
    encodeMPField (FMFile fname) = "; filename=" <> quoted fname
    quoted x = Builder.byteString ("\"" <> x <> "\"")
    kv k v = k <> ": " <> v
    newline = Builder.word8 (ord '\n')


data FileMeta = FMFormField -- ^ any form field except a file
              | FMFile ByteString -- ^ file name


ord :: Char -> Word8
ord = fromIntegral . Char.ord