spacecookie-1.1.0.1: test/Test/Integration.hs
{-# LANGUAGE OverloadedStrings #-}
module Test.Integration where
import Control.Applicative ((<|>))
import Control.Concurrent (threadDelay)
import Control.Exception (bracket)
import Control.Monad (forM_)
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.Char (ord)
import Data.List (sort)
import Data.Maybe (isNothing, isJust, fromJust)
import Network.Curl.Download (openURI)
import System.Directory (findExecutable)
import System.Environment (lookupEnv)
import System.Exit (ExitCode (..))
import System.Process (spawnProcess, terminateProcess, waitForProcess)
import Test.Tasty
import Test.Tasty.ExpectedFailure
import Test.Tasty.HUnit
import Test.Tasty.Providers (testPassed)
import Test.Tasty.Runners (Result (..))
spacecookieBin :: IO (Maybe FilePath)
spacecookieBin = do
fromEnv <- lookupEnv "SPACECOOKIE_TEST_BIN"
fromPath <- findExecutable "spacecookie"
pure $ fromEnv <|> fromPath
ignoreTestIf :: IO Bool -> String -> TestTree -> TestTree
ignoreTestIf doSkip msg tree = wrapTest change tree
where change normal = do
skip <- doSkip
if not skip
then normal
else pure $ (testPassed msg) {
resultShortDescription = "SKIP"
}
integrationTests :: TestTree
integrationTests = testGroup "integration tests"
[ ignoreTestIf (isNothing <$> spacecookieBin) "no spacecookie executable"
$ testCaseSteps "spacecookie server behaves as expected" integrationAsserts
]
integrationAsserts :: (String -> IO ()) -> Assertion
integrationAsserts step = do
step "getting spacecookie executable"
bin <- spacecookieBin
assertBool "have spacecookie executable" $ isJust bin
step "starting spacecookie executable"
bracket (spawn (fromJust bin)) assertSuccess
$ const $ do
threadDelay 1000000 -- wait 1 sec for the server to start up
step "request root menu"
assertEqual "root menu as expected" (Right expectedRoot)
=<< openURI "gopher://localhost:7000/0"
assertEqual "root menu requested with / as expected" (Right expectedRoot)
=<< openURI "gopher://localhost:7000/0/"
step "request plain.txt"
fileDisk <- Right <$> B.readFile "test/integration/root/plain.txt"
fileGopher <- openURI "gopher://localhost:7000/1/plain.txt"
assertEqual "served file is same as on disk" fileDisk fileGopher
step "check automatically generated directory menus"
dir <- openURI "gopher://localhost:7000/0/dir"
dirNoSlash <- openURI "gopher://localhost:7000/0dir"
assertEqual "directory menu is equal regardless of request" dir dirNoSlash
-- ignore ordering for the purpose of this test
assertEqual "directory menu contains expected entries" (Right expectedDir)
$ sort . filter (not . B.null) . B.split (fromIntegral $ ord '\n') <$> dir
step "sanity check not found error messages"
notFoundError <- openURI "gopher://localhost:7000/1/does/not/exist"
anotherNotFoundError <- openURI "gopher://localhost:7000/0/not-here.txt"
urlNotFoundError <- openURI "gopher://localhost:7000/0URL:http://sterni.lv"
assertEqual "precise error message"
(Right expectedErrorMessage) anotherNotFoundError
forM_ [ notFoundError, anotherNotFoundError, urlNotFoundError ]
$ assertIsError
assertBool "error responses differ for different files"
$ notFoundError /= anotherNotFoundError
assertBool "error response for URL: selectors is helpful"
$ Right True == fmap (B.isInfixOf "support") urlNotFoundError
step "sanity check not allowed error messages"
-- can't test directory traversal since curl won't try it
accessGophermap <- openURI "gopher://localhost:7000/0/.gophermap"
accessNonExistentDot <- openURI "gopher://localhost:7000/0/dir/.not-here"
forM_ [ accessGophermap, accessNonExistentDot ] $ \err -> do
assertIsError err
assertBool "error response is not allowed response"
$ Right True == fmap (B.isInfixOf "allow") err
where spawn bin = spawnProcess bin [ "test/integration/spacecookie.json" ]
assertSuccess hdl = do
step "stopping spacecookie"
terminateProcess hdl
assertEqual "spacecookie's exit code indicates SIGTERM" (ExitFailure (-15))
=<< waitForProcess hdl
assertIsError e = assertEqual "error response starts with a 3" (Right "3")
$ fmap (B.take 1) e
expectedRoot :: ByteString
expectedRoot = mconcat
[ "iHello World!\tHello World!\tlocalhost\t7000\r\n"
, "i\t\tlocalhost\t7000\r\n"
, "0normal text file\t/plain.txt\tlocalhost\t7000\r\n"
, "1normal dir\t/dir\tlocalhost\t7000\r\n"
, "i\t\tlocalhost\t7000\r\n"
, "1external 1\t/\tthis.is.bogus.org\t7000\r\n"
, "1external 2\t/\tsdf.org\t70\r\n"
, ".\r\n"
]
expectedDir :: [ByteString]
expectedDir = sort
[ "1another\t/dir/another\tlocalhost\t7000\r"
, "0mystery-file\t/dir/mystery-file\tlocalhost\t7000\r"
, "0strange.tXT\t/dir/strange.tXT\tlocalhost\t7000\r"
, "4macintosh.hqx\t/dir/macintosh.hqx\tlocalhost\t7000\r"
, ".\r"
]
expectedErrorMessage :: ByteString
expectedErrorMessage = mconcat
[ "3The requested resource '/not-here.txt' does not exist"
, " or is not available.\tErr\tlocalhost\t7000\r\n"
, ".\r\n"
]