restman-0.7.5.0: test/BinarySpec.hs
{-# language OverloadedStrings #-}
-- | Acceptance tests that spawn the @restman@ binary and inspect its
-- behaviour via the local echo server.
--
-- These tests require the @restman@ executable to be present on @PATH@.
-- When running via @stack test@, Stack builds the executable first and adds
-- the package bin-dir to @PATH@ automatically.
module BinarySpec (tests) where
-- base
import Data.List (isInfixOf)
import System.Exit (ExitCode(..))
import System.Process (readProcessWithExitCode)
-- aeson
import Data.Aeson (FromJSON(..), eitherDecode, withObject, (.:))
-- bytestring
import qualified Data.ByteString.Lazy.Char8 as BL8
-- containers
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
-- directory
import System.Directory (findExecutable)
-- text
import qualified Data.Text as T
-- tasty
import Test.Tasty
import Test.Tasty.HUnit
-- warp
import Network.Wai.Handler.Warp (Port)
-- local
import HTTP.EchoServer (withEchoServer)
-- ---------------------------------------------------------------------------
-- EchoResponse — mirrors the JSON produced by HTTP.EchoServer
-- ---------------------------------------------------------------------------
data EchoResponse = EchoResponse
{ echoMethod :: T.Text
, echoHeaders :: Map T.Text T.Text
, echoBody :: T.Text
} deriving (Eq, Show)
instance FromJSON EchoResponse where
parseJSON = withObject "EchoResponse" $ \o ->
EchoResponse
<$> o .: "method"
<*> o .: "headers"
<*> o .: "body"
-- ---------------------------------------------------------------------------
-- Helpers
-- ---------------------------------------------------------------------------
-- | Locate the @restman@ binary on @PATH@, failing the test if it is absent.
findRestmanExe :: IO FilePath
findRestmanExe = do
mPath <- findExecutable "restman"
case mPath of
Nothing -> assertFailure
"restman binary not found in PATH; run acceptance tests via 'stack test'"
Just p -> pure p
-- | Run @restman@ with the supplied arguments, returning
-- @(exitCode, stdout, stderr)@.
runRestman :: [String] -> IO (ExitCode, String, String)
runRestman args = do
exe <- findRestmanExe
readProcessWithExitCode exe args ""
echoURL :: Port -> String -> String
echoURL port path = "http://localhost:" <> show port <> path
-- | Run @restman --oneshot@ against the local echo server with optional
-- extra arguments, decode the JSON response body, and return the result.
oneshotEcho :: Port -> [String] -> IO EchoResponse
oneshotEcho port extraArgs = do
let args = extraArgs ++ ["--oneshot", echoURL port "/"]
(code, out, err) <- runRestman args
case code of
ExitFailure n ->
assertFailure $ "restman exited with code " <> show n
<> "; stderr: " <> err
ExitSuccess ->
case eitherDecode (BL8.pack out) of
Left e -> assertFailure
$ "Failed to decode echo response: " <> e
<> "\nstdout was: " <> out
Right r -> pure r
-- ---------------------------------------------------------------------------
-- Tests
-- ---------------------------------------------------------------------------
tests :: TestTree
tests = testGroup "Application (acceptance)"
[ cliTests
, fetchTests
]
cliTests :: TestTree
cliTests = testGroup "CLI"
[ testCase "--help exits with success and mentions RESTman" $ do
(code, out, _) <- runRestman ["--help"]
code @?= ExitSuccess
assertBool ("expected 'RESTman' in help output\ngot: " <> out)
("RESTman" `isInfixOf` out)
, testCase "unrecognised flag exits with failure" $ do
(code, _, _) <- runRestman ["--not-a-real-flag"]
case code of
ExitFailure _ -> pure ()
ExitSuccess -> assertFailure
"expected non-zero exit for unrecognised flag"
, testCase "--oneshot without URI exits with failure" $ do
(code, _, err) <- runRestman ["--oneshot"]
case code of
ExitFailure _ ->
assertBool ("expected 'requires' in error output\ngot: " <> err)
("requires" `isInfixOf` err)
ExitSuccess ->
assertFailure
"expected non-zero exit when --oneshot has no URI"
]
fetchTests :: TestTree
fetchTests = testGroup "fetch (--oneshot)"
[ testCase "default method is GET" $ withEchoServer $ \port -> do
er <- oneshotEcho port []
echoMethod er @?= "GET"
, testCase "-m DELETE sends DELETE" $ withEchoServer $ \port -> do
er <- oneshotEcho port ["-m", "DELETE"]
echoMethod er @?= "DELETE"
, testCase "-m POST with --payload sends request body" $ withEchoServer $ \port -> do
er <- oneshotEcho port ["-m", "POST", "--payload", "ping"]
echoBody er @?= "ping"
, testCase "-h sends a custom header" $ withEchoServer $ \port -> do
er <- oneshotEcho port ["-h", "X-Smoke:hello"]
Map.lookup "x-smoke" (echoHeaders er) @?= Just "hello"
, testCase "default headers include user-agent" $ withEchoServer $ \port -> do
er <- oneshotEcho port []
assertBool "expected 'user-agent' header with default settings"
(Map.member "user-agent" (echoHeaders er))
, testCase "--no-default-headers omits user-agent" $ withEchoServer $ \port -> do
er <- oneshotEcho port ["--no-default-headers"]
assertBool "expected 'user-agent' to be absent with --no-default-headers"
(Map.notMember "user-agent" (echoHeaders er))
]