packages feed

katip-elasticsearch-0.4.2.0: test/Main.hs

{-# LANGUAGE CPP                 #-}
{-# LANGUAGE DeriveDataTypeable  #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE KindSignatures      #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE RankNTypes          #-}
{-# LANGUAGE RecordWildCards     #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell     #-}
{-# LANGUAGE TypeFamilies        #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Main
    ( main
    ) where


-------------------------------------------------------------------------------
import           Control.Applicative                     as A
import           Control.Concurrent.STM
import           Control.Exception
import           Control.Lens                            hiding (mapping, (.=))
import           Control.Monad
import           Control.Monad.IO.Class
import           Data.Aeson
import           Data.Aeson.Lens
import           Data.Aeson.Types
import           Data.ByteString.Lazy                    (ByteString)
import qualified Data.HashMap.Strict                     as HM
import           Data.Monoid                             as M
import           Data.Scientific
import           Data.Tagged
import           Data.Text                               (Text)
import           Data.Time
import           Data.Time.Calendar.WeekDate
import           Data.Typeable                           as Typeable
import qualified Data.Vector                             as V
import qualified Database.V1.Bloodhound                  as V1
import qualified Database.V5.Bloodhound                  as V5
import           Network.HTTP.Client
import           Network.HTTP.Types.Status
import           Test.QuickCheck.Instances               ()
import           Test.Tasty
import           Test.Tasty.HUnit
import           Test.Tasty.Options
import           Test.Tasty.QuickCheck
-------------------------------------------------------------------------------
import           Katip
import           Katip.Scribes.ElasticSearch.Annotations
import           Katip.Scribes.ElasticSearch.Internal
-------------------------------------------------------------------------------


main :: IO ()
main = defaultMainWithIngredients ings $ askOption $ \vers -> testGroup "katip-elasticsearch"
  [
    case vers of
      TestV1 -> esTests (Typeable.Proxy :: Typeable.Proxy ESV1)
      TestV5 -> esTests (Typeable.Proxy :: Typeable.Proxy ESV5)
  , typeAnnotatedTests
  , roundToSundayTests
  ]
  where
    ings = (includingOptions [Option (Typeable.Proxy :: Typeable.Proxy TestWithESVersion)]):defaultIngredients


-------------------------------------------------------------------------------
data TestWithESVersion = TestV1
                       | TestV5
                       deriving (Typeable)


instance IsOption TestWithESVersion where
  defaultValue = TestV1
  parseValue "1" = Just TestV1
  parseValue "5" = Just TestV5
  parseValue _   = Nothing
  optionName = Tagged "es-version"
  optionHelp = Tagged "Version of ES to test against, either 1 or 5, defaulting to 1."


class ESVersion v => TestESVersion v where
  type Server v
  toServer :: proxy v -> Text -> Server v
  toMappingName :: proxy v -> Text -> MappingName v
  type Search v
  type Query v
  type Filter v
  mkSearch :: proxy v -> Maybe (Query v) -> Maybe (Filter v) -> Search v
  mkBHEnv :: proxy v -> Server v -> Manager -> BHEnv v
  type ShardCount v
  toShardCount :: proxy v -> Int -> ShardCount v
  type ReplicaCount v
  toReplicaCount :: proxy v -> Int -> ReplicaCount v
  indexShards :: proxy v -> Lens' (IndexSettings v) (ShardCount v)
  indexReplicas :: proxy v -> Lens' (IndexSettings v) (ReplicaCount v)

  deleteIndex :: proxy v -> IndexName v -> BH v IO (Response ByteString)
  deleteTemplate :: proxy v -> TemplateName v -> BH v IO (Response ByteString)
  refreshIndex :: proxy v -> IndexName v -> BH v IO (Response ByteString)
  withBH :: proxy v -> ManagerSettings -> Server v -> BH v IO a -> IO a
  searchByIndex :: proxy v -> IndexName v -> Search v -> BH v IO (Response ByteString)


instance TestESVersion ESV1 where
  type Server ESV1 = V1.Server
  toServer _ = V1.Server
  toMappingName _ = V1.MappingName
  type Search ESV1 = V1.Search
  type Query ESV1 = V1.Query
  type Filter ESV1 = V1.Filter
  type ShardCount ESV1 = V1.ShardCount
  toShardCount _ = V1.ShardCount
  type ReplicaCount ESV1 = V1.ReplicaCount
  toReplicaCount _ = V1.ReplicaCount
  mkSearch _ = V1.mkSearch
  mkBHEnv _ = V1.mkBHEnv
  indexShards _ = lens V1.indexShards (\s v -> s { V1.indexShards = v})
  indexReplicas _ = lens V1.indexReplicas (\r v -> r { V1.indexReplicas = v})

  deleteIndex _ = V1.deleteIndex
  deleteTemplate _ = V1.deleteTemplate
  refreshIndex _ = V1.refreshIndex
  withBH _ = V1.withBH
  searchByIndex _ = V1.searchByIndex


instance TestESVersion ESV5 where
  type Server ESV5 = V5.Server
  toServer _ = V5.Server
  toMappingName _ = V5.MappingName
  type Search ESV5 = V5.Search
  type Query ESV5 = V5.Query
  type Filter ESV5 = V5.Filter
  type ShardCount ESV5 = V5.ShardCount
  toShardCount _ = V5.ShardCount
  type ReplicaCount ESV5 = V5.ReplicaCount
  toReplicaCount _ = V5.ReplicaCount
  mkSearch _ = V5.mkSearch
  mkBHEnv _ = V5.mkBHEnv
  indexShards _ = lens V5.indexShards (\s v -> s { V5.indexShards = v})
  indexReplicas _ = lens V5.indexReplicas (\r v -> r { V5.indexReplicas = v})

  deleteIndex _ = V5.deleteIndex
  deleteTemplate _ = V5.deleteTemplate
  refreshIndex _ = V5.refreshIndex
  withBH _ = V5.withBH
  searchByIndex _ = V5.searchByIndex


-------------------------------------------------------------------------------
setupSearch
  :: forall proxy v. ( TestESVersion v
                     , MonadIO (BH v IO)
#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ < 800
                     , Functor (BH v IO)
#endif
                     )
  => proxy v
  -> (EsScribeCfg v -> EsScribeCfg v)
  -> IO Scribe
setupSearch prx modScribeCfg = do
    bh prx (dropESSchema prx)
    mgr <- newManager defaultManagerSettings
    mkEsScribe cfg (mkBHEnv prx (svr prx) mgr) (ixn prx) (mn prx) DebugS V3
  where
    cfg :: EsScribeCfg v
    cfg = modScribeCfg $
      (defaultEsScribeCfg' prx)
        { essAnnotateTypes = True
        , essIndexSettings = ixs prx
        }


-------------------------------------------------------------------------------
teardownSearch
  :: ( TestESVersion v
     , Monad (BH v IO)
#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ < 800
     , Functor (BH v IO)
#endif
     )
  => proxy v
  -> IO ()
teardownSearch prx = do
  bh prx $ do
    dropESSchema prx
    dropESSTemplate prx


-------------------------------------------------------------------------------
withSearch
  :: ( TestESVersion v
     , MonadIO (BH v IO)
#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ < 800
     , Functor (BH v IO)
#endif
     )
  => proxy v
  -> (IO Scribe -> TestTree)
  -> TestTree
withSearch = withSearch' id


-------------------------------------------------------------------------------
withSearch'
  :: ( TestESVersion v
     , MonadIO (BH v IO)
#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ < 800
     , Functor (BH v IO)
#endif
     )
  => (EsScribeCfg v -> EsScribeCfg v)
  -> proxy v
  -> (IO Scribe -> TestTree)
  -> TestTree
withSearch' modScribeCfg prx = withResource (setupSearch prx modScribeCfg) (const (teardownSearch prx))


-------------------------------------------------------------------------------
esTests
  :: ( TestESVersion v
     , MonadIO (BH v IO)
#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ < 800
     , Functor (BH v IO)
#endif
     , Show (IndexName v)
     )
  => proxy v
  -> TestTree
esTests prx = testGroup "elasticsearch scribe"
  [
    withSearch' (\c -> c { essIndexSharding = NoIndexSharding}) prx $ \setup -> testCase "it flushes to elasticsearch" $ withTestLogging prx setup $ \done -> do
       $(logT) (ExampleCtx True) M.mempty InfoS "A test message"
       liftIO $ do
         void done
         logs <- getLogs prx
         length logs @?= 1
         let l = head logs
         l ^? key "_source" . key "msg" . _String @?= Just "A test message"
         l ^? key "_source" . key "data" . key "whatever::b" . _Bool @?= Just True
  , testCase "can set up twice with no sharding" $ do
      let setup = bracket_ (setupSearch prx (\c -> c { essIndexSharding = NoIndexSharding })) (teardownSearch prx)
      setup $ setup $
        return ()
  , testCase "can set up twice with sharding" $ do
      let setup = bracket_ (setupSearch prx (\c -> c { essIndexSharding = DailyIndexSharding })) (teardownSearch prx)
      setup $ setup $
        return ()
  , withSearch prx $ \setup -> testCase "date-based index sharding" $ do
      let t1 = mkTime 2016 1 2 3 4 5
      fakeClock <- newTVarIO t1
      withTestLogging' (set logEnvTimer (readTVarIO fakeClock)) prx setup $ \done -> do
        $(logT) (ExampleCtx True) mempty InfoS "today"
        let t2 = mkTime 2016 1 3 3 4 5
        liftIO (atomically (writeTVar fakeClock t2))
        $(logT) (ExampleCtx True) mempty InfoS "tomorrow"
        liftIO $ do
          void done
          todayLogs <- getLogsByIndex prx (toIndexName prx "katip-elasticsearch-tests-2016-01-02")
          tomorrowLogs <- getLogsByIndex prx (toIndexName prx "katip-elasticsearch-tests-2016-01-03")
          assertBool ("todayLogs has " <> show (length todayLogs) <> " items") (length todayLogs == 1)
          assertBool ("tomorrowLogs has " <> show (length tomorrowLogs) <> " items") (length tomorrowLogs == 1)
          let logToday = head todayLogs
          let logTomorrow = head tomorrowLogs
          logToday ^? key "_source" . key "msg" . _String @?= Just "today"
          logTomorrow ^? key "_source" . key "msg" . _String @?= Just "tomorrow"
  , withSearch' (\c -> c { essIndexSharding = WeeklyIndexSharding}) prx $ \setup -> testCase "weekly index sharding rounds to previous sunday" $ do
      let t1 = mkTime 2016 3 5 0 0 0 -- saturday, march 5th
      fakeClock <- newTVarIO t1
      withTestLogging' (set logEnvTimer (readTVarIO fakeClock)) prx setup $ \done -> do
        $(logT) (ExampleCtx True) mempty InfoS "today"
        let t2 = mkTime 2016 3 6 0 0 0 -- sunday march 6th
        liftIO (atomically (writeTVar fakeClock t2))
        $(logT) (ExampleCtx True) mempty InfoS "tomorrow"
        liftIO $ do
          void done
          todayLogs <- getLogsByIndex prx (toIndexName prx "katip-elasticsearch-tests-2016-02-28") -- rounds back to previous sunday
          tomorrowLogs <- getLogsByIndex prx (toIndexName prx "katip-elasticsearch-tests-2016-03-06") -- is on sunday, so uses current date
          assertBool ("todayLogs has " <> show (length todayLogs) <> " items") (length todayLogs == 1)
          assertBool ("tomorrowLogs has " <> show (length tomorrowLogs) <> " items") (length tomorrowLogs == 1)
          let logToday = head todayLogs
          let logTomorrow = head tomorrowLogs
          logToday ^? key "_source" . key "msg" . _String @?= Just "today"
          logTomorrow ^? key "_source" . key "msg" . _String @?= Just "tomorrow"
  ]


-------------------------------------------------------------------------------
mkTime :: Integer -> Int -> Int -> DiffTime -> DiffTime -> DiffTime -> UTCTime
mkTime y m d hr minute s = UTCTime day dt
  where
    day = mkDay y m d
    dt = s + 60 * minute + 60 * 60 * hr


-------------------------------------------------------------------------------
mkDay :: Integer -> Int -> Int -> Day
mkDay y m d = day
  where
    Just day = fromGregorianValid y m d


-------------------------------------------------------------------------------
data ExampleCtx = ExampleCtx {
      ecBool :: Bool
    }

instance ToJSON ExampleCtx where
  toJSON c = object ["whatever" .= ecBool c]


instance ToObject ExampleCtx


instance LogItem ExampleCtx where
  payloadKeys _ _ = AllKeys

-------------------------------------------------------------------------------
typeAnnotatedTests :: TestTree
typeAnnotatedTests = testGroup "TypeAnnotated"
  [
    testCase "annotates values on toJSON" $
      toJSON (TypeAnnotated exampleValue) @?= annotatedExampleValue

  , testCase "annotates values on toObject" $
      toObject (TypeAnnotated exampleObject) @?= annotatedExampleObject

  , testCase "deannotates on parseJSON" $
      parseEither parseJSON (toJSON exampleValue) @?= Right exampleValue

  , testProperty "roundtrips the same as raw" $ \(v :: Value) ->
      let res = typeAnnotatedValue
                <$> parseEither parseJSON (toJSON (TypeAnnotated v))
      in res === Right v
  ]


-------------------------------------------------------------------------------
roundToSundayTests :: TestTree
roundToSundayTests = testGroup "roundToSunday"
  [
    testProperty "always returns a sunday" $ \d ->
      getDOW (roundToSunday d) === 7
  , testProperty "returns input on sunday" $ \d -> getDOW d == 7 ==>
      roundToSunday d === d
  , testProperty "goes back a week when not sunday" $ \d -> getDOW d /= 7 ==>
      roundToSunday d < d
  ]
  where
    getDOW = view _3 . toWeekDate


-------------------------------------------------------------------------------
exampleObject :: Object
exampleObject = HM.fromList
  [ ("a bool", Bool False)
  , ("a long", Number 24)
  , ("a double", Number 52.3)
  , ("a string", String "s")
  , ("a null", Null)
  , ("a map", Object (HM.singleton "baz" (Bool True)))
  ]


-------------------------------------------------------------------------------
annotatedExampleObject :: Object
annotatedExampleObject = HM.fromList
  [ ("a map",Object $ HM.fromList [("baz::b", Bool True)])
  , ("a bool::b", Bool False)
  , ("a null::n", Null)
  , ("a string::s", String "s")
  , ("a double::d", Number 52.3)
  , ("a long::l", Number 24.0)
  ]


-------------------------------------------------------------------------------
exampleValue :: Value
exampleValue = Array $ V.fromList [Null, Object exampleObject]


-------------------------------------------------------------------------------
annotatedExampleValue :: Value
annotatedExampleValue = Array $ V.fromList
  [ Null
  , Object annotatedExampleObject
  ]


-------------------------------------------------------------------------------
getLogs
  :: ( TestESVersion v
     , Monad (BH v IO)
#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ < 800
     , Functor (BH v IO)
#endif
     , Show (IndexName v)
     )
  => proxy v
  -> IO [Value]
getLogs prx = getLogsByIndex prx (ixn prx)


-------------------------------------------------------------------------------
getLogsByIndex
  :: ( TestESVersion v
     , Monad (BH v IO)
#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ < 800
     , Functor (BH v IO)
#endif
     , Show (IndexName v)
     )
  => proxy v
  -> IndexName v
  -> IO [Value]
getLogsByIndex prx i = do
  r <- bh prx $ do
    void (refreshIndex prx i)
    searchByIndex prx i (mkSearch prx Nothing Nothing)
  let actualCode = statusCode (responseStatus r)
  assertBool ("search by " <> show i <> " " <> show actualCode <> " /= 200") (actualCode == 200)
  return $ responseBody r ^.. key "hits" . key "hits" . values


-------------------------------------------------------------------------------
bh :: TestESVersion v => proxy v -> BH v IO a -> IO a
bh prx = withBH prx defaultManagerSettings (svr prx)


-------------------------------------------------------------------------------
withTestLogging
  :: TestESVersion v
  => proxy v
  -> IO Scribe
  -> (IO (Response ByteString) -> KatipT IO b)
  -> IO b
withTestLogging = withTestLogging' id


-------------------------------------------------------------------------------
withTestLogging'
  :: (TestESVersion v)
  => (LogEnv -> LogEnv)
  -> proxy v
  -> IO Scribe
  -> (IO (Response ByteString) -> KatipT IO b)
  -> IO b
withTestLogging' modEnv prx setup f = do
  scr <- setup
  le <- modEnv <$> initLogEnv ns env
  le' <- registerScribe "es" scr defaultScribeSettings le
  let done' = do
        _ <- closeScribes le'
        bh prx (refreshIndex prx (ixn prx))
  runKatipT le' (f done')
  where
    ns = Namespace ["katip-test"]
    env = Environment "test"


-------------------------------------------------------------------------------
svr :: TestESVersion v => proxy v -> Server v
svr prx = toServer prx  "http://localhost:9200"


-------------------------------------------------------------------------------
ixn :: TestESVersion v => proxy v -> IndexName v
ixn prx = toIndexName prx "katip-elasticsearch-tests"


-------------------------------------------------------------------------------
ixs :: TestESVersion v => proxy v -> IndexSettings v
ixs prx = defaultIndexSettings prx
  & indexShards prx .~ toShardCount prx 1
  & indexReplicas prx .~ toReplicaCount prx 1


-------------------------------------------------------------------------------
tn :: TestESVersion v => proxy v -> TemplateName v
tn prx = toTemplateName prx "katip-elasticsearch-tests"


-------------------------------------------------------------------------------
mn :: TestESVersion v => proxy v -> MappingName v
mn prx = toMappingName prx "logs"


-------------------------------------------------------------------------------
dropESSchema
  :: ( TestESVersion v
     , Monad (BH v IO)
#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ < 800
     , Functor (BH v IO)
#endif
     )
  => proxy v
  -> BH v IO ()
dropESSchema prx = void $ deleteIndex prx (toIndexName prx "katip-elasticsearch-tests*")


-------------------------------------------------------------------------------
dropESSTemplate
  :: ( TestESVersion v
     , Monad (BH v IO)
#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ < 800
     , Functor (BH v IO)
#endif
     )
  => proxy v
  -> BH v IO ()
dropESSTemplate prx = void $ deleteTemplate prx (tn prx)


-------------------------------------------------------------------------------
instance Arbitrary Value where
  arbitrary = oneof
    [ Object <$> reduceSize arbitrary
    , Array . V.fromList <$> reduceSize arbitrary
    , String <$> arbitrary
    , Number <$> (scientific <$> arbitrary <*> arbitrary)
    , Bool <$> arbitrary
    , A.pure Null
    ]


-------------------------------------------------------------------------------
-- | Reduce the size of Arbitrary input for the given generator
reduceSize :: Gen a -> Gen a
reduceSize f = sized $ \ n -> resize (n `div` 2) f