packages feed

bloodhound-1.0.0.0: tests/Test/RolloverSpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}

module Test.RolloverSpec where

import Control.Exception (finally)
import Data.Aeson.KeyMap qualified as KM
import Data.Map.Strict qualified as M
import Data.Text qualified as T
import Database.Bloodhound.Common.Client qualified as Client
import TestsUtils.Common
import TestsUtils.Import

-- Test fixture: the alias @bh-rollover@ points at the write index
-- @bh-rollover-000001@. After 'rolloverIndex' the alias is repointed
-- at @bh-rollover-000002@ and the new index is created.
rolloverAliasName :: IndexAliasName
rolloverAliasName = IndexAliasName [qqIndexName|bh-rollover|]

firstIndexName :: IndexName
firstIndexName = [qqIndexName|bh-rollover-000001|]

secondIndexName :: IndexName
secondIndexName = [qqIndexName|bh-rollover-000002|]

-- | Build the @cioAliases@ block that marks @bh-rollover@ as the write
-- index for @bh-rollover-000001@. The rollover API refuses to operate
-- on an alias that does not have a designated write index.
writeIndexAlias :: KM.KeyMap Value
writeIndexAlias =
  KM.singleton "bh-rollover" (toJSON defaultIndexAliasCreate {aliasCreateIsWriteIndex = Just True})

-- | Tear down any indices + alias left over from a previous run. Both
-- possible target indices are deleted because rollover may have
-- advanced the suffix in an earlier run.
cleanupRollover :: BH IO ()
cleanupRollover = do
  _ <- tryEsError $ performBHRequest $ deleteIndex firstIndexName
  _ <- tryEsError $ performBHRequest $ deleteIndex secondIndexName
  pure ()

-- | Create @bh-rollover-000001@ with the alias @bh-rollover@ already
-- attached as the write index (see 'writeIndexAlias'). The rollover
-- API refuses to operate on an alias that does not have a designated
-- write index.
seedRolloverAlias :: BH IO ()
seedRolloverAlias = do
  _ <- cleanupRollover
  let settings = IndexSettings (ShardCount 1) (ReplicaCount 0) defaultIndexMappingsLimits
  let opts =
        defaultCreateIndexOptions
          { cioSettings = Just settings,
            cioAliases = Just writeIndexAlias
          }
  _ <- performBHRequest $ createIndexOptions opts firstIndexName
  pure ()

-- | 'True' when the conditions map contains a key mentioning the given
-- substring with the given boolean value. The server renders each
-- threshold as a display string (e.g. @"[max_docs: 1]"@) rather than
-- echoing the request shape.
conditionsHas :: T.Text -> Bool -> Maybe (M.Map T.Text Bool) -> Bool
conditionsHas substr val =
  maybe
    False
    ( any
        ( \(k, v) ->
            T.isInfixOf substr k && v == val
        )
        . M.toList
    )

-- Test body for the conditions-met case. Exported so the spec can wrap
-- it in 'finally' for exception-safe cleanup.
conditionsMetAction :: BH IO ()
conditionsMetAction = do
  _ <- seedRolloverAlias
  _ <-
    performBHRequest $
      indexDocument
        firstIndexName
        defaultIndexDocumentSettings
        exampleTweet
        (DocId "1")
  _ <- performBHRequest $ refreshIndex firstIndexName
  resp <-
    Client.rolloverIndex
      rolloverAliasName
      (Just (defaultRolloverConditions {rolloverConditionsMaxDocs = Just 1}))
  liftIO $ do
    rolloverResponseAcknowledged resp `shouldBe` True
    rolloverResponseShardsAcknowledged resp `shouldBe` True
    rolloverResponseRolledOver resp `shouldBe` True
    rolloverResponseDryRun resp `shouldBe` False
    rolloverResponseOldIndex resp `shouldBe` Just firstIndexName
    rolloverResponseNewIndex resp `shouldBe` Just secondIndexName
    conditionsHas "max_docs" True (rolloverResponseConditions resp) `shouldBe` True

-- Test body for the unconditional-rollover case.
unconditionalAction :: BH IO ()
unconditionalAction = do
  _ <- seedRolloverAlias
  resp <- Client.rolloverIndex rolloverAliasName Nothing
  liftIO $ do
    rolloverResponseAcknowledged resp `shouldBe` True
    rolloverResponseRolledOver resp `shouldBe` True
    rolloverResponseOldIndex resp `shouldBe` Just firstIndexName
    rolloverResponseNewIndex resp `shouldBe` Just secondIndexName
    -- With no conditions supplied, no threshold should be marked met.
    let noMetThresholds = maybe True (all not . M.elems) (rolloverResponseConditions resp)
    noMetThresholds `shouldBe` True

-- Test body for the conditions-not-met case.
conditionsNotMetAction :: BH IO ()
conditionsNotMetAction = do
  _ <- seedRolloverAlias
  resp <-
    Client.rolloverIndex
      rolloverAliasName
      (Just (defaultRolloverConditions {rolloverConditionsMaxDocs = Just 100}))
  liftIO $ do
    rolloverResponseAcknowledged resp `shouldBe` False
    rolloverResponseRolledOver resp `shouldBe` False
    rolloverResponseOldIndex resp `shouldBe` Just firstIndexName
    rolloverResponseNewIndex resp `shouldBe` Just secondIndexName
    conditionsHas "max_docs" False (rolloverResponseConditions resp) `shouldBe` True

spec :: Spec
spec = do
  describe "rolloverIndex API" $ do
    -- Successful rollover: max_docs=1 is satisfied by indexing exactly
    -- one document before the call. ES swaps the alias to the new index
    -- and reports both old_index and new_index. The server echoes the
    -- condition back as a @Map Text Bool@ keyed by a human-readable
    -- description like @"[max_docs: 1]"@.
    --
    -- 'finally' ensures the indices are torn down even if an assertion
    -- throws, so a failed run does not leak @bh-rollover-000001@ /
    -- @bh-rollover-000002@ onto the server.
    it "rolls the alias over to the next index when conditions are met" $
      withTestEnv conditionsMetAction `finally` withTestEnv cleanupRollover

    -- Rollover without any conditions: the server creates the new index
    -- unconditionally and repoints the alias. This is the @Nothing@
    -- branch of the public API.
    it "rolls over unconditionally when conditions are Nothing" $
      withTestEnv unconditionalAction `finally` withTestEnv cleanupRollover

    -- Rollover conditions not satisfied: max_docs is set higher than
    -- the actual document count. The server reports @acknowledged=false@
    -- (no rollover performed) and @rolled_over=false@, and @conditions@
    -- shows the unmet threshold. The server still populates
    -- @old_index@ / @new_index@ with the candidate names.
    it "reports rolled_over=False when conditions are not met" $
      withTestEnv conditionsNotMetAction `finally` withTestEnv cleanupRollover

    -- The conditions block is omitted from the body when 'Nothing' is
    -- passed; the body is the empty bytestring (not absent) so the
    -- server treats the call as an unconditional rollover.
    it "sends an empty body when conditions are Nothing" $ do
      let req = rolloverIndex rolloverAliasName Nothing
      bhRequestBody req `shouldBe` Just ""

    it "includes a conditions object when conditions are Just" $ do
      let conditions = defaultRolloverConditions {rolloverConditionsMaxDocs = Just 5}
      let req = rolloverIndex rolloverAliasName (Just conditions)
      let expected = object ["conditions" .= object ["max_docs" .= (5 :: Int)]]
      (decode =<< bhRequestBody req) `shouldBe` Just expected