packages feed

reload-0.0.0.1: test/Language/Haskell/Reload/FileBrowserSpec.hs

{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
module Language.Haskell.Reload.FileBrowserSpec (spec) where

import           Test.Hspec
import           Test.Hspec.Wai
import           Test.Hspec.Wai.JSON
import           Network.Wai.Test
import           Data.Aeson hiding (json)

import           Language.Haskell.Reload (app)
import           Language.Haskell.Reload.Config
import           System.Directory
import           System.FilePath
import           Data.List (sortBy, isPrefixOf)
import           Data.Text (Text,toLower)
import qualified Data.HashMap.Strict as HM
import           Data.Ord
import           Data.Maybe
import qualified Data.ByteString.Lazy as B
import qualified Data.ByteString.Char8 as B8
import           Data.Monoid
import           Control.Monad

spec :: Spec
spec = after cleanUp $ with (app False)$ do
  describe "GET /files" $ do
    it "responds with files and folders at root" $ do
      js <- liftIO $ do
          root <- getCurrentDirectory
          fs <- getDirectoryContents root
          mv <- config root
          let showHidden = showHiddenFiles mv
          let filt = if showHidden
                        then (\x->x/="." && x/="..")
                        else (not . ("." `isPrefixOf`))
          let visible = filter (filt . takeFileName) fs
          sortBy sortFs <$> mapM (fileToJson root) visible
      get "/files" `shouldRespondWith` (fromValue $ toJSON js)
      get "/files/" `shouldRespondWith` (fromValue $ toJSON js)
    it "get source root folder" $ do
      get "/files/src" `shouldRespondWith` [json|[{"type":"dir","path":"src/Language"}]|]
      get "/files/src/" `shouldRespondWith` [json|[{"type":"dir","path":"src/Language"}]|]
    it "doesn't accept .." $ do
      get "/files/../reload/src" `shouldRespondWith` 404
    it "doesn't accept absolutes" $ do
      get "/files//tmp" `shouldRespondWith` 404
  describe "GET /file" $ do
    it "get root plain file" $ do
      testFile "LICENSE" "text/plain"
    it "get root cabal file" $ do
      testFile "reload.cabal" "text/x-cabal"
    it "get root yaml file" $ do
      testFile "stack.yaml" "text/x-yaml"
    it "get src haskell file" $ do
      testFile "src/Language/Haskell/Reload.hs" "text/x-haskell"
    it "doesn't accept .." $ do
        get "/file/../reload/LICENSE" `shouldRespondWith` 404
    it "doesn't accept absolutes" $ do
        get "/files//etc/passwd" `shouldRespondWith` 404
  describe "PUT /files" $ do
    it "creates a new folder" $ do
      liftIO $ do
          root <- getCurrentDirectory
          ex <- doesDirectoryExist (root </> "tempTest")
          when ex $ do
            removeDirectoryRecursive (root </> "tempTest")
            ex2 <- doesDirectoryExist (root </> "tempTest")
            ex2 `shouldBe` False
      put "/files/tempTest" B.empty `shouldRespondWith` [json|null|] {matchStatus = 201}
      liftIO $ do
          root <- getCurrentDirectory
          ex <- doesDirectoryExist (root </> "tempTest")
          ex `shouldBe` True
      put "/files/tempTest" B.empty `shouldRespondWith` [json|null|] {matchStatus = 204}
    it "doesn't accept .." $ do
        put "/files/../tempTest" B.empty `shouldRespondWith` 404
    it "doesn't accept absolutes" $ do
        put "/files//tmp/tempTest" B.empty `shouldRespondWith` 404
  describe "DELETE /files" $ do
    it "deletes an existing folder" $ do
      liftIO $ do
          root <- getCurrentDirectory
          createDirectoryIfMissing True (root </> "tempTest")
          ex <- doesDirectoryExist (root </> "tempTest")
          ex `shouldBe` True
      delete "/files/tempTest" `shouldRespondWith` [json|null|] {matchStatus = 200}
      liftIO $ do
          root <- getCurrentDirectory
          ex <- doesDirectoryExist (root </> "tempTest")
          ex `shouldBe` False
      delete "/files/tempTest" `shouldRespondWith` [json|null|] {matchStatus = 204}
    it "doesn't accept .." $ do
        delete "/files/../tempTest" `shouldRespondWith` 404
    it "doesn't accept absolutes" $ do
        delete "/files//tmp/tempTest" `shouldRespondWith` 404
  describe "PUT /file" $ do
    it "creates and modifies a new file" $ do
      liftIO $ do
          root <- getCurrentDirectory
          createDirectoryIfMissing True (root </> "tempTest")
          ex <- doesDirectoryExist (root </> "tempTest")
          ex `shouldBe` True
      put "/file/tempTest/f.txt" "content" `shouldRespondWith` 201
      liftIO $ do
          root <- getCurrentDirectory
          f <- B.readFile (root </> "tempTest" </> "f.txt")
          f `shouldBe` "content"
      put "/file/tempTest/f.txt" "dummy" `shouldRespondWith` 200
      liftIO $ do
          root <- getCurrentDirectory
          f <- B.readFile (root </> "tempTest" </> "f.txt")
          f `shouldBe` "dummy"
    it "doesn't accept .." $ do
        put "/file/../tempTest/f.txt" "dummy" `shouldRespondWith` 404
    it "doesn't accept absolutes" $ do
        put "/file//tmp/f.txt" "dummy" `shouldRespondWith` 404
    it "doesn't accept directories" $ do
        put "/file/test" "dummy" `shouldRespondWith` 403
  describe "DELETE /file" $ do
    it "deletes a file" $ do
      liftIO $ do
          root <- getCurrentDirectory
          createDirectoryIfMissing True (root </> "tempTest")
          ex <- doesDirectoryExist (root </> "tempTest")
          ex `shouldBe` True
          let p = root </> "tempTest" </> "f.txt"
          B.writeFile p "content"
          exf <- doesFileExist p
          exf `shouldBe` True
      delete "/file/tempTest/f.txt" `shouldRespondWith` [json|null|] {matchStatus = 200}
      liftIO $ do
        root <- getCurrentDirectory
        let p = root </> "tempTest" </> "f.txt"
        exf <- doesFileExist p
        exf `shouldBe` False
      delete "/file/tempTest/f.txt" `shouldRespondWith` [json|null|] {matchStatus = 204}
    it "doesn't accept .." $ do
      liftIO $ do
          root <- getCurrentDirectory
          createDirectoryIfMissing True (root </> "tempTest")
          ex <- doesDirectoryExist (root </> "tempTest")
          ex `shouldBe` True
          let p = root </> "tempTest" </> "f.txt"
          B.writeFile p "content"
          exf <- doesFileExist p
          exf `shouldBe` True
      delete "/file/../reload/tempTest/f.txt" `shouldRespondWith` 404
    it "doesn't accept absolutes" $ do
      delete "/file//tmp/f.txt" `shouldRespondWith` 404

cleanUp :: () -> IO()
cleanUp _ = do
    root <- getCurrentDirectory
    ex <- doesDirectoryExist (root </> "tempTest")
    when ex $ do
      removeDirectoryRecursive (root </> "tempTest")

--testFile :: FilePath -> ByteString -
testFile fp ct = do
  l <- liftIO $ B.readFile fp
  let fpb = B8.pack fp
  get ("/file/" <> fpb) `shouldRespondWith` 200 {matchHeaders = ["Content-Type" <:> ct]}
  r <- simpleBody <$> get ("/file/" <> fpb)
  liftIO $ r `shouldBe` l

fileToJson :: FilePath -> FilePath -> IO Value
fileToJson root fs = do
    let full =root</> fs
    isf <- doesFileExist full
    return $ if isf
        then let
          m =  HM.lookupDefault "text/plain" (takeExtension fs) myMimes
          in object ["type" .= ("file"::Text), "path" .= fs, "mime" .= m]
        else object ["type" .= ("dir"::Text), "path" .= fs]

sortFs :: Value -> Value -> Ordering
sortFs (Object m1) (Object m2) =
  let (String t1) = fromJust $ HM.lookup "type" m1
      (String t2) = fromJust $ HM.lookup "type" m2
      tc = t1 `compare` t2
  in if tc == EQ
      then
        let (String p1) = fromJust $ HM.lookup "path" m1
            (String p2) = fromJust $ HM.lookup "path" m2
        in (toLower p1) `compare` (toLower p2)
      else tc

myMimes :: HM.HashMap String Text
myMimes = HM.insert ".yaml" "text/x-yaml" $
          HM.insert ".cabal" "text/x-cabal" $
          HM.insert ".lhs" "text/x-haskell" $
          HM.insert ".hs" "text/x-haskell" $
          HM.insert ".json" "application/json" HM.empty