{-# LANGUAGE BangPatterns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} module Snap.Util.FileServe.Tests ( tests ) where import Blaze.ByteString.Builder import Control.Monad import Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as S import qualified Data.ByteString.Lazy.Char8 as L import Data.IORef import qualified Data.Map as Map import Data.Maybe import Data.Monoid import Prelude hiding (take) import Test.Framework import Test.Framework.Providers.HUnit import Test.HUnit hiding (Test, path) import Snap.Internal.Http.Types import Snap.Internal.Types import Snap.Util.FileServe import Snap.Iteratee tests :: [Test] tests = [ testFs , testFsCfg , testFsSingle , testRangeOK , testRangeBad , testMultiRange , testIfRange ] expect404 :: IO Response -> IO () expect404 m = do r <- m assertBool "expected 404" (rspStatus r == 404) expect302 :: ByteString -> IO Response -> IO () expect302 p m = do r <- m assertBool "expected 302" (rspStatus r == 302) assertEqual "redir location" (Just p) (getHeader "location" r) getBody :: Response -> IO L.ByteString getBody r = do let benum = rspBodyToEnum $ rspBody r liftM (toLazyByteString . mconcat) (runIteratee consume >>= run_ . benum) runIt :: Snap a -> Request -> Iteratee ByteString IO (Request, Response) runIt m rq = runSnap m d d rq where d = const $ return () go :: Snap a -> ByteString -> IO Response go m s = do rq <- mkRequest s liftM snd (run_ $ runIt m rq) goIfModifiedSince :: Snap a -> ByteString -> ByteString -> IO Response goIfModifiedSince m s lm = do rq <- mkRequest s let r = setHeader "if-modified-since" lm rq liftM snd (run_ $ runIt m r) goIfRange :: Snap a -> ByteString -> (Int,Int) -> ByteString -> IO Response goIfRange m s (start,end) lm = do rq <- mkRequest s let r = setHeader "if-range" lm $ setHeader "Range" (S.pack $ "bytes=" ++ show start ++ "-" ++ show end) rq liftM snd (run_ $ runIt m r) goRange :: Snap a -> ByteString -> (Int,Int) -> IO Response goRange m s (start,end) = do rq' <- mkRequest s let rq = setHeader "Range" (S.pack $ "bytes=" ++ show start ++ "-" ++ show end) rq' liftM snd (run_ $ runIt m rq) goMultiRange :: Snap a -> ByteString -> (Int,Int) -> (Int,Int) -> IO Response goMultiRange m s (start,end) (start2,end2) = do rq' <- mkRequest s let rq = setHeader "Range" (S.pack $ "bytes=" ++ show start ++ "-" ++ show end ++ "," ++ show start2 ++ "-" ++ show end2) rq' liftM snd (run_ $ runIt m rq) goRangePrefix :: Snap a -> ByteString -> Int -> IO Response goRangePrefix m s start = do rq' <- mkRequest s let rq = setHeader "Range" (S.pack $ "bytes=" ++ show start ++ "-") rq' liftM snd (run_ $ runIt m rq) goRangeSuffix :: Snap a -> ByteString -> Int -> IO Response goRangeSuffix m s end = do rq' <- mkRequest s let rq = setHeader "Range" (S.pack $ "bytes=-" ++ show end) rq' liftM snd (run_ $ runIt m rq) mkRequest :: ByteString -> IO Request mkRequest uri = do enum <- newIORef $ SomeEnumerator returnI return $ Request "foo" 80 "foo" 999 "foo" 1000 "foo" False Map.empty enum Nothing GET (1,1) [] "" uri "/" (S.concat ["/",uri]) "" Map.empty fs :: Snap () fs = do x <- serveDirectory "data/fileServe" return $! x `seq` () fsSingle :: Snap () fsSingle = do x <- serveFile "data/fileServe/foo.html" return $! x `seq` () fsCfg :: DirectoryConfig Snap -> Snap () fsCfg cfg = do x <- serveDirectoryWith cfg "data/fileServe" return $! x `seq` () testFs :: Test testFs = testCase "fileServe/multi" $ do r1 <- go fs "foo.bin" b1 <- getBody r1 assertEqual "foo.bin" "FOO\n" b1 assertEqual "foo.bin content-type" (Just "application/octet-stream") (getHeader "content-type" r1) assertEqual "foo.bin size" (Just 4) (rspContentLength r1) assertBool "last-modified header" (isJust $ getHeader "last-modified" r1) assertEqual "accept-ranges header" (Just "bytes") (getHeader "accept-ranges" r1) let !lm = fromJust $ getHeader "last-modified" r1 -- check last modified stuff r2 <- goIfModifiedSince fs "foo.bin" lm assertEqual "foo.bin 304" 304 $ rspStatus r2 r3 <- goIfModifiedSince fs "foo.bin" "Wed, 15 Nov 1995 04:58:08 GMT" assertEqual "foo.bin 200" 200 $ rspStatus r3 b3 <- getBody r3 assertEqual "foo.bin 2" "FOO\n" b3 r4 <- go fs "foo.txt" b4 <- getBody r4 assertEqual "foo.txt" "FOO\n" b4 assertEqual "foo.txt content-type" (Just "text/plain") (getHeader "content-type" r4) r5 <- go fs "foo.html" b5 <- getBody r5 assertEqual "foo.html" "FOO\n" b5 assertEqual "foo.html content-type" (Just "text/html") (getHeader "content-type" r5) r6 <- go fs "foo.bin.bin.bin" b6 <- getBody r6 assertEqual "foo.bin.bin.bin" "FOO\n" b6 assertEqual "foo.bin.bin.bin content-type" (Just "application/octet-stream") (getHeader "content-type" r6) expect404 $ go fs "jfldksjflksd" expect404 $ go fs "dummy/../foo.txt" expect404 $ go fs "/etc/password" coverMimeMap testFsSingle :: Test testFsSingle = testCase "fileServe/Single" $ do r1 <- go fsSingle "foo.html" b1 <- getBody r1 assertEqual "foo.html" "FOO\n" b1 assertEqual "foo.html content-type" (Just "text/html") (getHeader "content-type" r1) assertEqual "foo.html size" (Just 4) (rspContentLength r1) testFsCfg :: Test testFsCfg = testCase "fileServe/Cfg" $ do let cfgA = DirectoryConfig { indexFiles = [], indexGenerator = const pass, dynamicHandlers = Map.empty, mimeTypes = defaultMimeTypes } -- Named file in the root directory rA1 <- go (fsCfg cfgA) "foo.bin" bA1 <- getBody rA1 assertEqual "A1" "FOO\n" bA1 assertEqual "A1 content-type" (Just "application/octet-stream") (getHeader "content-type" rA1) -- Missing file in the root directory expect404 $ go (fsCfg cfgA) "bar.bin" -- Named file in a subdirectory rA2 <- go (fsCfg cfgA) "mydir2/foo.txt" bA2 <- getBody rA2 assertEqual "A2" "FOO\n" bA2 assertEqual "A2 content-type" (Just "text/plain") (getHeader "content-type" rA2) -- Missing file in a subdirectory expect404 $ go (fsCfg cfgA) "mydir2/bar.txt" -- Request for directory with no trailing slash expect302 "/mydir1/" $ go (fsCfg cfgA) "mydir1" -- Request for directory with trailing slash, no index expect404 $ go (fsCfg cfgA) "mydir1/" expect404 $ go (fsCfg cfgA) "mydir2/" -- Request file with trailing slash expect404 $ go (fsCfg cfgA) "foo.html/" expect404 $ go (fsCfg cfgA) "mydir2/foo.txt/" let cfgB = DirectoryConfig { indexFiles = ["index.txt", "altindex.html"], indexGenerator = const pass, dynamicHandlers = Map.empty, mimeTypes = defaultMimeTypes } -- Request for root directory with index rB1 <- go (fsCfg cfgB) "mydir1/" bB1 <- getBody rB1 assertEqual "B1" "INDEX\n" bB1 assertEqual "B1 content-type" (Just "text/plain") (getHeader "content-type" rB1) -- Request for root directory with alternate index rB2 <- go (fsCfg cfgB) "mydir3/" bB2 <- getBody rB2 assertEqual "B2" "ALTINDEX\n" bB2 assertEqual "B2 content-type" (Just "text/html") (getHeader "content-type" rB2) -- Request for root directory with no index expect404 $ go (fsCfg cfgB) "mydir2/" let printName c = writeBS $ snd $ S.breakEnd (=='/') $ S.pack c let cfgC = DirectoryConfig { indexFiles = ["index.txt", "altindex.html"], indexGenerator = printName, dynamicHandlers = Map.empty, mimeTypes = defaultMimeTypes } -- Request for root directory with index rC1 <- go (fsCfg cfgC) "mydir1/" bC1 <- getBody rC1 assertEqual "C1" "INDEX\n" bC1 assertEqual "C1 content-type" (Just "text/plain") (getHeader "content-type" rC1) -- Request for root directory with generated index rC2 <- go (fsCfg cfgC) "mydir2/" bC2 <- getBody rC2 assertEqual "C2" "mydir2" bC2 let cfgD = DirectoryConfig { indexFiles = [], indexGenerator = const pass, dynamicHandlers = Map.fromList [ (".txt", printName) ], mimeTypes = defaultMimeTypes } -- Request for file with dynamic handler rD1 <- go (fsCfg cfgD) "mydir2/foo.txt" bD1 <- getBody rD1 assertEqual "D1" "foo.txt" bD1 -- Request for directory with autogen index rE1 <- go (fsCfg fancyDirectoryConfig) "mydir2/" bE1 <- S.concat `fmap` L.toChunks `fmap` getBody rE1 assertBool "autogen-sub-index" $ "Directory Listing: /mydir2/" `S.isInfixOf` bE1 assertBool "autogen-sub-parent" $ " m () coverMimeMap = Prelude.mapM_ f $ Map.toList defaultMimeTypes where f (!k,!v) = return $ case k `seq` v `seq` () of () -> () testIfRange :: Test testIfRange = testCase "fileServe/range/if-range" $ do r <- goIfRange fs "foo.bin" (1,2) "Wed, 15 Nov 1995 04:58:08 GMT" assertEqual "foo.bin 200" 200 $ rspStatus r b <- getBody r assertEqual "foo.bin" "FOO\n" b r2 <- goIfRange fs "foo.bin" (1,2) "Tue, 01 Oct 2030 04:58:08 GMT" assertEqual "foo.bin 206" 206 $ rspStatus r2 b2 <- getBody r2 assertEqual "foo.bin partial" "OO" b2 r3 <- goIfRange fs "foo.bin" (1,24324) "Tue, 01 Oct 2030 04:58:08 GMT" assertEqual "foo.bin 200" 200 $ rspStatus r3 b3 <- getBody r3 assertEqual "foo.bin" "FOO\n" b3