{-# 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)) ]