packages feed

katip-elasticsearch-0.4.0.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.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
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) 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
  , 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