packages feed

couch-simple-0.0.1.0: test/Functionality/Util.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}

module Functionality.Util where

import           Control.Monad                    (liftM, return, (>=>), (>>=))
import           Control.Monad.IO.Class           (MonadIO, liftIO)
import           Data.Aeson                       (FromJSON, Value (Object),
                                                   decode)
import           Data.ByteString.Lazy             (readFile)
import           Data.Default                     (def)
import           Data.Either                      (Either (Left, Right))
import           Data.Eq                          ((==))
import           Data.Function                    (const, id, ($), (.))
import           Data.Functor                     (fmap, (<$>))
import           Data.JsonSchema                  (RawSchema (..), compile,
                                                   draft4, validate)
import           Data.Maybe                       (Maybe (Just, Nothing))
import           Data.Monoid                      (mempty, (<>))
import           Data.String                      (IsString, String, fromString,
                                                   unwords)
import           Data.UUID                        (toString)
import qualified Database.Couch.Explicit.Database as Database (create, delete)
import qualified Database.Couch.Response          as Response (asBool)
import           Database.Couch.Types             (Context (Context), Error,
                                                   Port (Port), Result)
import           GHC.Err                          (error)
import           Network.HTTP.Client              (Manager,
                                                   defaultManagerSettings,
                                                   newManager)
import           System.Directory                 (doesFileExist,
                                                   getCurrentDirectory)
import           System.FilePath                  (takeDirectory, (</>))
import           System.IO                        (FilePath, IO)
import           System.Random                    (randomIO)
import           Test.Tasty                       (TestName, TestTree,
                                                   defaultMain, testGroup,
                                                   withResource)
import           Test.Tasty.HUnit                 (assertFailure, testCaseSteps,
                                                   (@=?))
import           Text.Show                        (show)

dbContext :: MonadIO m => IO Manager -> m Context
dbContext getManager = do
  manager <- liftIO getManager
  uuid <- liftM (fromString . ("test-" <>) . toString) (liftIO randomIO)
  return $ Context manager "localhost" (Port 5984) Nothing def (Just uuid)

serverContext :: MonadIO m => IO Manager -> m Context
serverContext getManager = do
  manager <- liftIO getManager
  return $ Context manager "localhost" (Port 5984) Nothing def Nothing

releaseContext :: Context -> IO ()
releaseContext = const $ return ()

runTests :: (IO Manager -> TestTree) -> IO ()
runTests testTree = do
  let manager = newManager defaultManagerSettings
  defaultMain $ testTree manager

testAgainstFailure :: String
                   -> (Context -> IO (Result Value))
                   -> Error
                   -> IO Context
                   -> TestTree
testAgainstFailure desc function exception getContext = testCaseSteps desc $ \step -> do
  step "Make request"
  getContext >>= function >>= checkException step exception

checkException :: (String -> IO ())
               -> Error
               -> Result Value
               -> IO ()
checkException step exception res = do
  step "Got an exception"
  case res of
    -- HttpException isn't Eqable, so we simply coerce with show
    Left err -> show exception @=? show err
    Right val -> assertFailure $ unwords
                                   [ "Didn't get expected exception"
                                   , show exception
                                   , "instead"
                                   , show val
                                   ]

throwOnError :: FromJSON a => Result a -> IO ()
throwOnError res =
  case res of
    Left err -> error $ show err
    Right _  -> return ()

withDb :: (IO Context -> TestTree) -> IO Context -> TestTree
withDb test getContext =
  withResource
    (getContext >>= createTempDb)
    (fmap Response.asBool . Database.delete >=> throwOnError)
    test
  where
    createTempDb ctx = do
      Response.asBool <$> Database.create ctx >>= throwOnError
      return ctx

testAgainstSchema :: String
                  -> (Context -> IO (Result Value))
                  -> FilePath
                  -> IO Context
                  -> TestTree
testAgainstSchema desc function schema =
  testAgainstSchemaAndValue desc function schema id (const . const (return ()))

testAgainstSchemaAndValue :: String
                          -> (Context -> IO (Result Value))
                          -> FilePath
                          -> (Result Value -> Result a)
                          -> ((String -> IO ()) -> a -> IO ())
                          -> IO Context
                          -> TestTree
testAgainstSchemaAndValue desc function schema decoder checker getContext = testCaseSteps desc $ \step -> do
  step "Make request"
  getContext >>= function >>= checkCookiesAndSchema step schema decoder checker

checkCookiesAndSchema :: (String -> IO ())
                      -> FilePath
                      -> (Result Value -> Result a)
                      -> ((String -> IO ()) -> a -> IO ())
                      -> Result Value
                      -> IO ()
checkCookiesAndSchema step schemaFile decoder checker res = do
  step "No exception"
  case res of
    Left err -> assertFailure (show err)
    Right (json, cookieJar) -> do
      step "Empty cookie jar"
      def @=? cookieJar
      checkSchema step json schemaFile
      step $ "Decoding json: " <> show json
      case decoder res of
        Left err -> assertFailure (show err)
        Right (val, _) -> do
          step "Checking value"
          checker step val

checkSchema :: IsString s => (s -> IO ()) -> Value -> FilePath -> IO ()
checkSchema step value schemaName = do
  step "Checking result against schema"
  schema <- loadSchema ("test/schema/schema" </> schemaName)
  case validate (compile draft4 mempty schema) value of
    Left err -> assertFailure $ unwords ["Failed to validate", show value, ":", show err]
    Right _  -> return ()

loadSchema :: FilePath -> IO RawSchema
loadSchema file = do
  (_, content) <- findRequestedFile
  case decode content of
    Just (Object o) -> return RawSchema { _rsURI = "", _rsObject = o }
    _               -> error "Couldn't extract object from file"

  where
    findRequestedFile = do
      start <- getCurrentDirectory
      checkDir start
    checkDir dir =
      let path = dir </> file
      in doesFileExist path >>= \exists -> if exists
                                             then readFile path >>= \c -> return (path, c)
                                             else let upOne = takeDirectory dir
                                                  in if upOne == dir
                                                       then error "Cannot find file to embed as resource"
                                                       else checkDir upOne

class TestInput a where
  makeTests :: TestInput a => TestName -> [IO Context -> TestTree] -> a -> TestTree
  makeTests desc tests input = testGroup desc $ fmap (applyInput input) tests

  applyInput :: a -> (IO Context -> TestTree) -> TestTree

instance TestInput (IO Manager) where
  applyInput input = ($ dbContext input)

instance TestInput (IO Context) where
  applyInput input = ($ input)