bloodhound-1.0.0.0: tests/Test/ClusterStateSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Test.ClusterStateSpec where
import Data.List qualified as L
import Data.List.NonEmpty qualified as NE
import Data.Text qualified as T
import Database.Bloodhound.Common.Client qualified as Client
import Database.Bloodhound.Common.Requests qualified as Common
import TestsUtils.Common
import TestsUtils.Import
spec :: Spec
spec = do
describe "cluster state API" $ do
it "returns cluster state information" $
withTestEnv $ do
state <- Client.getClusterState
liftIO $ clusterStateClusterName state `shouldSatisfy` (not . T.null)
liftIO $ clusterStateClusterUuid state `shouldSatisfy` (not . T.null)
liftIO $ clusterStateVersion state `shouldSatisfy` (> 0)
it "returns nodes information" $
withTestEnv $ do
state <- Client.getClusterState
liftIO $ clusterStateNodes state `shouldSatisfy` (not . null)
-- ------------------------------------------------------------------ --
-- renderClusterStateMetric: pure wire-name rendering (no ES required). --
-- Locks in the exact text ES/OS expect for each metric so a typo or --
-- rename surfaces here rather than as a silent 400 from the server. --
-- ------------------------------------------------------------------ --
describe "renderClusterStateMetric" $ do
it "renders every constructor at its canonical wire name" $
L.sort (map renderClusterStateMetric [minBound .. maxBound])
`shouldBe` L.sort
[ "blocks",
"custom_metadata",
"master_node",
"metadata",
"nodes",
"routing_nodes",
"routing_table",
"settings",
"version"
]
it "renderClusterStateMetric is injective across constructors" $
-- Catches a copy/paste typo where two constructors render to the
-- same wire name (would silently collapse the metric filter).
noDuplicates (map renderClusterStateMetric [minBound .. maxBound])
-- ------------------------------------------------------------------ --
-- clusterStateOptionsPathSegments + clusterStateOptionsParams: --
-- pure URI rendering, no live backend needed. Mirrors the --
-- ClusterSettingsSpec "URI rendering" block. --
-- ------------------------------------------------------------------ --
describe "ClusterStateOptions URI rendering" $ do
let normalize :: [(T.Text, Maybe T.Text)] -> [(T.Text, Maybe T.Text)]
normalize = L.sort
it "defaultClusterStateOptions emits no path segments and no params" $ do
clusterStateOptionsPathSegments defaultClusterStateOptions `shouldBe` []
clusterStateOptionsParams defaultClusterStateOptions `shouldBe` []
it "renders a single metric as a path segment" $ do
let opts = defaultClusterStateOptions {cstoMetrics = Just (NE.fromList [ClusterStateMetricMetadata])}
clusterStateOptionsPathSegments opts `shouldBe` ["metadata"]
it "renders multiple metrics as a comma-joined path segment" $ do
let opts =
defaultClusterStateOptions
{ cstoMetrics =
Just $
NE.fromList [ClusterStateMetricMetadata, ClusterStateMetricNodes]
}
clusterStateOptionsPathSegments opts `shouldBe` ["metadata,nodes"]
it "renders multi-metric then multi-index as two CSV path segments, in order" $ do
-- Locks in the path-segment ordering when both filters carry
-- multiple values: the comma-joined metric segment precedes
-- the comma-joined index segment. A regression that swaps the
-- order, or flattens both into one CSV, surfaces here.
let opts =
defaultClusterStateOptions
{ cstoMetrics =
Just $
NE.fromList [ClusterStateMetricMetadata, ClusterStateMetricVersion],
cstoIndices = Just (NE.fromList [testIndex, otherIndex])
}
clusterStateOptionsPathSegments opts
`shouldBe` [ "metadata,version",
unIndexName testIndex <> "," <> unIndexName otherIndex
]
it "renders the index filter only when a metric filter is also set" $ do
-- Without a metric segment, GET /_cluster/state//foo is a 400.
-- The renderer must drop a lone index filter rather than emit
-- an empty path segment.
let withMetrics =
defaultClusterStateOptions
{ cstoMetrics = Just (NE.fromList [ClusterStateMetricMetadata]),
cstoIndices = Just (NE.fromList [testIndex])
}
withoutMetrics =
defaultClusterStateOptions
{ cstoIndices = Just (NE.fromList [testIndex])
}
clusterStateOptionsPathSegments withMetrics
`shouldBe` ["metadata", unIndexName testIndex]
clusterStateOptionsPathSegments withoutMetrics `shouldBe` []
it "renders multiple indices as a comma-joined path segment" $ do
let opts =
defaultClusterStateOptions
{ cstoMetrics = Just (NE.fromList [ClusterStateMetricRoutingTable]),
cstoIndices = Just (NE.fromList [testIndex, otherIndex])
}
clusterStateOptionsPathSegments opts
`shouldBe` ["routing_table", unIndexName testIndex <> "," <> unIndexName otherIndex]
it "renders local as lowercase true/false" $ do
let trueOpts = defaultClusterStateOptions {cstoLocal = Just True}
falseOpts = defaultClusterStateOptions {cstoLocal = Just False}
clusterStateOptionsParams trueOpts `shouldBe` [("local", Just "true")]
clusterStateOptionsParams falseOpts `shouldBe` [("local", Just "false")]
it "renders master_timeout as <n><suffix>" $ do
let opts = defaultClusterStateOptions {cstoMasterTimeout = Just (TimeUnitSeconds, 30)}
clusterStateOptionsParams opts `shouldBe` [("master_timeout", Just "30s")]
it "renders wait_for_metadata_version as a decimal" $ do
let opts = defaultClusterStateOptions {cstoWaitForMetadataVersion = Just 42}
clusterStateOptionsParams opts
`shouldBe` [("wait_for_metadata_version", Just "42")]
it "renders filter_path as a comma-joined list" $ do
let opts =
defaultClusterStateOptions
{ cstoFilterPath = Just (NE.fromList ["cluster_name", "metadata.indices.*"])
}
clusterStateOptionsParams opts
`shouldBe` [("filter_path", Just "cluster_name,metadata.indices.*")]
it "emits path segments and query params together when all are set" $ do
let opts =
defaultClusterStateOptions
{ cstoMetrics = Just (NE.fromList [ClusterStateMetricMetadata]),
cstoIndices = Just (NE.fromList [testIndex]),
cstoLocal = Just True,
cstoMasterTimeout = Just (TimeUnitMilliseconds, 500),
cstoWaitForMetadataVersion = Just 7,
cstoFilterPath = Just (NE.fromList ["metadata.indices.*"])
}
clusterStateOptionsPathSegments opts
`shouldBe` ["metadata", unIndexName testIndex]
normalize (clusterStateOptionsParams opts)
`shouldBe` [ ("filter_path", Just "metadata.indices.*"),
("local", Just "true"),
("master_timeout", Just "500ms"),
("wait_for_metadata_version", Just "7")
]
-- ------------------------------------------------------------------ --
-- getClusterStateWith endpoint shape: pure checks against the --
-- BHRequest value, no live backend needed. Mirrors the --
-- updateClusterSettings shape tests in Test.ClusterSettingsSpec. --
-- ------------------------------------------------------------------ --
describe "getClusterStateWith endpoint shape" $ do
it "GETs /_cluster/state with no extra path or query by default" $ do
let req = Common.getClusterStateWith defaultClusterStateOptions
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_cluster", "state"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "forwards metric and index filters as additional path segments" $ do
let opts =
defaultClusterStateOptions
{ cstoMetrics = Just (NE.fromList [ClusterStateMetricNodes]),
cstoIndices = Just (NE.fromList [testIndex])
}
req = Common.getClusterStateWith opts
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_cluster", "state", "nodes", unIndexName testIndex]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "forwards local, master_timeout and filter_path via the query string" $ do
let opts =
defaultClusterStateOptions
{ cstoLocal = Just True,
cstoMasterTimeout = Just (TimeUnitSeconds, 30),
cstoFilterPath = Just (NE.fromList ["master_node"])
}
req = Common.getClusterStateWith opts
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_cluster", "state"]
getRawEndpointQueries (bhRequestEndpoint req)
`shouldBe` [ ("local", Just "true"),
("master_timeout", Just "30s"),
("filter_path", Just "master_node")
]
-- | Local-only fixture index name so the spec doesn't depend on the
-- cluster's test-index bootstrap beyond 'testIndex'. Pure tests only
-- ever render this to text, never send it.
otherIndex :: IndexName
otherIndex = [qqIndexName|bloodhound-tests-twitter-2|]