bloodhound-1.0.0.0: tests/Test/CatSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Test.CatSpec (spec) where
import Control.Monad.Catch (bracket_)
import Data.Aeson.Key (fromText)
import Data.Aeson.KeyMap qualified as KM
import Data.Text qualified as T
import Database.Bloodhound.Client.Cluster (BackendType (..))
import Database.Bloodhound.Common.Client qualified as Client
import TestsUtils.Common
import TestsUtils.Import
import Prelude
-- Pure rendering + decoding tests for the cat indices option type and
-- response row. Mirrors the structure of "Test.ClusterHealthSpec": a
-- "URI rendering" describe block that does not need a running ES, plus
-- a JSON-parsing describe block driven by realistic cat wire payloads.
spec :: Spec
spec = do
-- ------------------------------------------------------------------ --
-- catIndicesOptionsParams: pure URI rendering (no ES required) --
-- ------------------------------------------------------------------ --
describe "catIndicesOptionsParams URI rendering" $ do
it "defaultCatIndicesOptions emits only format=json" $
catIndicesOptionsParams defaultCatIndicesOptions
`shouldBe` [("format", Just "json")]
it "renders bytes via catBytesSizeText" $ do
let opts = defaultCatIndicesOptions {catoBytes = Just CatBytesKb}
catIndicesOptionsParams opts
`shouldContain` [("bytes", Just "kb")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatIndicesOptions
{ catoColumns = Just (CatColIndex :| [CatColDocsCount, CatColStoreSize])
}
catIndicesOptionsParams opts
`shouldContain` [("h", Just "index,docs.count,store.size")]
it "renders unknown columns verbatim" $ do
let opts =
defaultCatIndicesOptions
{ catoColumns = Just (CatColOther "rep.throttle.time" :| [])
}
catIndicesOptionsParams opts
`shouldContain` [("h", Just "rep.throttle.time")]
it "renders expand_wildcards as a comma-joined list" $ do
let opts =
defaultCatIndicesOptions
{ catoExpandWildcards = Just (ExpandWildcardsOpen :| [ExpandWildcardsClosed])
}
catIndicesOptionsParams opts
`shouldContain` [("expand_wildcards", Just "open,closed")]
it "renders health via catHealthFilterText" $ do
let opts = defaultCatIndicesOptions {catoHealth = Just CatHealthFilterYellow}
catIndicesOptionsParams opts
`shouldContain` [("health", Just "yellow")]
it "renders booleans as true/false" $ do
let opts =
defaultCatIndicesOptions
{ catoHelp = Just True,
catoIncludeUnloadedSegments = Just False,
catoLocal = Just True,
catoPri = Just False,
catoVerbose = Just True
}
let ps = catIndicesOptionsParams opts
ps `shouldContain` [("help", Just "true")]
ps `shouldContain` [("include_unloaded_segments", Just "false")]
ps `shouldContain` [("local", Just "true")]
ps `shouldContain` [("pri", Just "false")]
ps `shouldContain` [("v", Just "true")]
it "renders master_timeout via timeUnitsSuffix" $ do
let opts = defaultCatIndicesOptions {catoMasterTimeout = Just (TimeUnitSeconds, 30)}
catIndicesOptionsParams opts
`shouldContain` [("master_timeout", Just "30s")]
it "renders sort specs as comma-joined column[:direction] terms" $ do
let opts =
defaultCatIndicesOptions
{ catoSort =
Just
( CatSortSpec CatColDocsCount (Just CatSortDesc)
:| [CatSortSpec CatColIndex Nothing]
)
}
catIndicesOptionsParams opts
`shouldContain` [("s", Just "docs.count:desc,index")]
-- ------------------------------------------------------------------ --
-- Renderer sanity for the leaf sum types --
-- ------------------------------------------------------------------ --
describe "CatBytesSize rendering" $
it "covers every documented unit" $
map
catBytesSizeText
[ CatBytesBytes,
CatBytesK,
CatBytesKb,
CatBytesM,
CatBytesMb,
CatBytesG,
CatBytesGb,
CatBytesT,
CatBytesTb,
CatBytesP,
CatBytesPb
]
`shouldBe` ["b", "k", "kb", "m", "mb", "g", "gb", "t", "tb", "p", "pb"]
-- ------------------------------------------------------------------ --
-- CatIndicesRow JSON decoding --
-- ------------------------------------------------------------------ --
describe "CatIndicesRow FromJSON" $ do
let fullRow =
"{\
\ \"health\":\"green\",\
\ \"status\":\"open\",\
\ \"index\":\"bloodhound-tests-cat\",\
\ \"uuid\":\"XYZ123\",\
\ \"pri\":\"1\",\
\ \"rep\":\"1\",\
\ \"docs.count\":\"42\",\
\ \"docs.deleted\":\"3\",\
\ \"store.size\":\"5kb\",\
\ \"pri.store.size\":\"5kb\",\
\ \"dataset\":\"time-series\"\
\}"
minimalRow =
"{\
\ \"index\":\"bloodhound-tests-cat\"\
\}"
subsetRow =
"{\
\ \"index\":\"bloodhound-tests-cat\",\
\ \"docs.count\":\"100\",\
\ \"docs.deleted\":\"0\"\
\}"
fractionalRow =
"{\
\ \"index\":\"bloodhound-tests-cat\",\
\ \"store.size\":\"5.2gb\",\
\ \"pri.store.size\":\"5.2gb\"\
\}"
bareRow =
"{\
\ \"index\":\"bloodhound-tests-cat\",\
\ \"store.size\":\"208\",\
\ \"pri.store.size\":\"208\"\
\}"
unknownStatusRow =
"{\
\ \"index\":\"bloodhound-tests-cat\",\
\ \"status\":\"warm\"\
\}"
extraColumnRow =
"{\
\ \"index\":\"bloodhound-tests-cat\",\
\ \"memory.total\":\"1gb\"\
\}"
badUnitRow =
"{\
\ \"index\":\"bloodhound-tests-cat\",\
\ \"store.size\":\"5qb\"\
\}"
it "parses a fully-populated default row" $
case parseEither parseJSON =<< eitherDecode fullRow :: Either String CatIndicesRow of
Right row -> do
cirHealth row `shouldBe` Just CatIndexHealthGreen
cirStatus row `shouldBe` Just CatIndexStatusOpen
cirIndex row `shouldBe` [qqIndexName|bloodhound-tests-cat|]
cirUuid row `shouldBe` Just "XYZ123"
cirPrimaryShards row `shouldBe` Just 1
cirReplicaCount row `shouldBe` Just 1
cirDocsCount row `shouldBe` Just 42
cirDocsDeleted row `shouldBe` Just 3
cirStoreSize row `shouldBe` Just (Bytes 5, CatBytesKb)
cirPrimaryStoreSize row `shouldBe` Just (Bytes 5, CatBytesKb)
cirDataset row `shouldBe` Just "time-series"
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses a minimal row containing only `index`" $
case parseEither parseJSON =<< eitherDecode minimalRow :: Either String CatIndicesRow of
Right row -> do
cirIndex row `shouldBe` [qqIndexName|bloodhound-tests-cat|]
cirHealth row `shouldBe` Nothing
cirStatus row `shouldBe` Nothing
cirDocsCount row `shouldBe` Nothing
cirStoreSize row `shouldBe` Nothing
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses a column-subset row (as produced by h=index,docs.count,docs.deleted)" $
case parseEither parseJSON =<< eitherDecode subsetRow :: Either String CatIndicesRow of
Right row -> do
cirIndex row `shouldBe` [qqIndexName|bloodhound-tests-cat|]
cirDocsCount row `shouldBe` Just 100
cirDocsDeleted row `shouldBe` Just 0
cirStoreSize row `shouldBe` Nothing
Left e -> expectationFailure ("expected parse, got " <> e)
it "rounds fractional magnitudes to the nearest whole byte" $
case parseEither parseJSON =<< eitherDecode fractionalRow :: Either String CatIndicesRow of
Right row -> do
cirStoreSize row `shouldBe` Just (Bytes 5, CatBytesGb)
cirPrimaryStoreSize row `shouldBe` Just (Bytes 5, CatBytesGb)
Left e -> expectationFailure ("expected parse, got " <> e)
it "treats a bare number as CatBytesBytes" $
case parseEither parseJSON =<< eitherDecode bareRow :: Either String CatIndicesRow of
Right row ->
cirStoreSize row `shouldBe` Just (Bytes 208, CatBytesBytes)
Left e -> expectationFailure ("expected parse, got " <> e)
it "preserves unknown status values via CatIndexStatusOther" $
case parseEither parseJSON =<< eitherDecode unknownStatusRow :: Either String CatIndicesRow of
Right row -> cirStatus row `shouldBe` Just (CatIndexStatusOther "warm")
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in cirOther" $
case parseEither parseJSON =<< eitherDecode extraColumnRow :: Either String CatIndicesRow of
Right row ->
-- 'cirOther' is the original JSON object with every key the
-- server sent, including ones we model and ones we don't.
case cirOther row of
Object o -> KM.keys o `shouldContain` ["index", "memory.total"]
_ -> expectationFailure "expected cirOther to be an Object"
Left e -> expectationFailure ("expected parse, got " <> e)
it "rejects an unrecognised byte-size suffix" $
case parseEither parseJSON =<< eitherDecode badUnitRow :: Either String CatIndicesRow of
Right row -> expectationFailure ("expected parse failure, got " <> show row)
Left _ -> return ()
-- Defensive coverage: if OpenSearch emits a native JSON number for
-- an integer column (instead of the usual string), the parser
-- should still succeed by stringifying the integer-valued
-- Scientific without a trailing ".0". See 'parseAsString'.
it "tolerates a native JSON number for an integer column" $
case parseEither parseJSON =<< eitherDecode "{\"index\":\"x\",\"docs.count\":42}" :: Either String CatIndicesRow of
Right row -> cirDocsCount row `shouldBe` Just 42
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates a native JSON number for store.size (treated as bare bytes)" $
case parseEither parseJSON =<< eitherDecode "{\"index\":\"x\",\"store.size\":208}" :: Either String CatIndicesRow of
Right row -> cirStoreSize row `shouldBe` Just (Bytes 208, CatBytesBytes)
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catSizeInBytes --
-- ------------------------------------------------------------------ --
describe "catSizeInBytes" $ do
it "treats bare bytes as identity" $
catSizeInBytes (Bytes 208, CatBytesBytes) `shouldBe` Bytes 208
it "applies the base-10 multiplier for k/m/g" $ do
catSizeInBytes (Bytes 5, CatBytesK) `shouldBe` Bytes 5000
catSizeInBytes (Bytes 5, CatBytesM) `shouldBe` Bytes 5000000
catSizeInBytes (Bytes 5, CatBytesG) `shouldBe` Bytes 5000000000
it "applies the base-2 multiplier for kb/mb/gb" $ do
catSizeInBytes (Bytes 5, CatBytesKb) `shouldBe` Bytes 5120
catSizeInBytes (Bytes 5, CatBytesMb) `shouldBe` Bytes 5242880
catSizeInBytes (Bytes 5, CatBytesGb) `shouldBe` Bytes 5368709120
-- ------------------------------------------------------------------ --
-- Smoke test against a real backend (mirrors Test.ClusterHealthSpec) --
-- ------------------------------------------------------------------ --
describe "catIndicesWith integration" $ do
it "returns a row for an index we just created" $
withTestEnv $
withCatIndex [qqIndexName|bloodhound-tests-cat-indices|] $ \idx -> do
rows <- Client.catIndicesWith defaultCatIndicesOptions
liftIO $
case filter ((== idx) . cirIndex) rows of
(row : _) -> do
cirIndex row `shouldBe` idx
cirStatus row `shouldSatisfy` (`elem` [Just CatIndexStatusOpen, Just CatIndexStatusClosed])
[] ->
expectationFailure "expected at least one row for the freshly-created index"
it "honours the h= parameter and leaves unrequested columns absent" $
withTestEnv $
withCatIndex [qqIndexName|bloodhound-tests-cat-indices-col|] $ \idx -> do
let opts =
defaultCatIndicesOptions
{ catoColumns = Just (CatColIndex :| [CatColDocsCount])
}
rows <- Client.catIndicesWith opts
liftIO $
case filter ((== idx) . cirIndex) rows of
(row : _) -> do
cirIndex row `shouldBe` idx
cirDocsCount row `shouldSatisfy` isJust
-- Columns we did not request should be absent:
cirHealth row `shouldBe` Nothing
cirUuid row `shouldBe` Nothing
cirStoreSize row `shouldBe` Nothing
[] ->
expectationFailure "expected at least one row for the freshly-created index"
-- ------------------------------------------------------------------ --
-- catAliasesOptionsParams: pure URI rendering (no ES required) --
-- ------------------------------------------------------------------ --
describe "catAliasesOptionsParams URI rendering" $ do
it "defaultCatAliasesOptions emits only format=json" $
catAliasesOptionsParams defaultCatAliasesOptions
`shouldBe` [("format", Just "json")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatAliasesOptions
{ caaColumns = Just (CatAliasColAlias :| [CatAliasColIndex, CatAliasColIsWriteIndex])
}
catAliasesOptionsParams opts
`shouldContain` [("h", Just "alias,index,is_write_index")]
it "renders unknown columns verbatim" $ do
let opts =
defaultCatAliasesOptions
{ caaColumns = Just (CatAliasColOther "ts" :| [])
}
catAliasesOptionsParams opts
`shouldContain` [("h", Just "ts")]
it "renders expand_wildcards as a comma-joined list" $ do
let opts =
defaultCatAliasesOptions
{ caaExpandWildcards = Just (ExpandWildcardsOpen :| [ExpandWildcardsClosed])
}
catAliasesOptionsParams opts
`shouldContain` [("expand_wildcards", Just "open,closed")]
it "renders booleans as true/false" $ do
let opts =
defaultCatAliasesOptions
{ caaHelp = Just True,
caaLocal = Just False,
caaVerbose = Just True
}
let ps = catAliasesOptionsParams opts
ps `shouldContain` [("help", Just "true")]
ps `shouldContain` [("local", Just "false")]
ps `shouldContain` [("v", Just "true")]
it "renders master_timeout via timeUnitsSuffix" $ do
let opts = defaultCatAliasesOptions {caaMasterTimeout = Just (TimeUnitSeconds, 30)}
catAliasesOptionsParams opts
`shouldContain` [("master_timeout", Just "30s")]
it "renders sort specs as comma-joined column[:direction] terms" $ do
let opts =
defaultCatAliasesOptions
{ caaSort =
Just
( CatAliasesSortSpec CatAliasColAlias (Just CatSortDesc)
:| [CatAliasesSortSpec CatAliasColIndex Nothing]
)
}
catAliasesOptionsParams opts
`shouldContain` [("s", Just "alias:desc,index")]
-- ------------------------------------------------------------------ --
-- Renderer sanity for the leaf sum types --
-- ------------------------------------------------------------------ --
describe "catAliasesColumnText rendering" $
it "covers every documented column" $
map
catAliasesColumnText
[ CatAliasColAlias,
CatAliasColIndex,
CatAliasColFilter,
CatAliasColRoutingIndex,
CatAliasColRoutingSearch,
CatAliasColIsWriteIndex
]
`shouldBe` ["alias", "index", "filter", "routing.index", "routing.search", "is_write_index"]
describe "catAliasesIsWriteIndexToText / FromText round-trip" $ do
it "encodes Nothing as \"-\"" $
catAliasesIsWriteIndexToText Nothing `shouldBe` "-"
it "encodes Just True / Just False as \"true\" / \"false\"" $ do
catAliasesIsWriteIndexToText (Just True) `shouldBe` "true"
catAliasesIsWriteIndexToText (Just False) `shouldBe` "false"
it "round-trips every documented sentinel through FromText" $ do
catAliasesIsWriteIndexFromText "-" `shouldBe` Just Nothing
catAliasesIsWriteIndexFromText "true" `shouldBe` Just (Just True)
catAliasesIsWriteIndexFromText "false" `shouldBe` Just (Just False)
it "rejects unknown sentinels" $
catAliasesIsWriteIndexFromText "maybe" `shouldBe` Nothing
-- ------------------------------------------------------------------ --
-- CatAliasesRow JSON decoding --
-- ------------------------------------------------------------------ --
describe "CatAliasesRow FromJSON" $ do
let fullRow =
"{\
\ \"alias\":\"bloodhound-tests-cat-alias-full\",\
\ \"index\":\"bloodhound-tests-cat-aliases-src\",\
\ \"filter\":\"*-*\",\
\ \"routing.index\":\"1\",\
\ \"routing.search\":\"2\",\
\ \"is_write_index\":\"true\"\
\}"
minimalRow =
"{\
\ \"alias\":\"bloodhound-tests-cat-alias-min\",\
\ \"index\":\"bloodhound-tests-cat-aliases-src\"\
\}"
noWriteIndexRow =
"{\
\ \"alias\":\"bloodhound-tests-cat-alias-nowrite\",\
\ \"index\":\"bloodhound-tests-cat-aliases-src\",\
\ \"is_write_index\":\"-\"\
\}"
falseWriteIndexRow =
"{\
\ \"alias\":\"bloodhound-tests-cat-alias-false\",\
\ \"index\":\"bloodhound-tests-cat-aliases-src\",\
\ \"is_write_index\":\"false\"\
\}"
nativeBoolRow =
"{\
\ \"alias\":\"bloodhound-tests-cat-alias-native\",\
\ \"index\":\"bloodhound-tests-cat-aliases-src\",\
\ \"is_write_index\":true\
\}"
extraColumnRow =
"{\
\ \"alias\":\"bloodhound-tests-cat-alias-extra\",\
\ \"index\":\"bloodhound-tests-cat-aliases-src\",\
\ \"ts\":\"1700000000000\"\
\}"
badWriteIndexRow =
"{\
\ \"alias\":\"bloodhound-tests-cat-alias-bad\",\
\ \"index\":\"bloodhound-tests-cat-aliases-src\",\
\ \"is_write_index\":\"maybe\"\
\}"
it "parses a fully-populated default row" $
case parseEither parseJSON =<< eitherDecode fullRow :: Either String CatAliasesRow of
Right row -> do
carAlias row `shouldBe` [qqIndexName|bloodhound-tests-cat-alias-full|]
carIndex row `shouldBe` [qqIndexName|bloodhound-tests-cat-aliases-src|]
carFilter row `shouldBe` Just "*-*"
carRoutingIndex row `shouldBe` Just "1"
carRoutingSearch row `shouldBe` Just "2"
carIsWriteIndex row `shouldBe` Just True
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses a minimal row containing only alias and index" $
case parseEither parseJSON =<< eitherDecode minimalRow :: Either String CatAliasesRow of
Right row -> do
carAlias row `shouldBe` [qqIndexName|bloodhound-tests-cat-alias-min|]
carIndex row `shouldBe` [qqIndexName|bloodhound-tests-cat-aliases-src|]
carFilter row `shouldBe` Nothing
carRoutingIndex row `shouldBe` Nothing
carRoutingSearch row `shouldBe` Nothing
carIsWriteIndex row `shouldBe` Nothing
Left e -> expectationFailure ("expected parse, got " <> e)
it "decodes the \"-\" sentinel for is_write_index as Nothing" $
case parseEither parseJSON =<< eitherDecode noWriteIndexRow :: Either String CatAliasesRow of
Right row -> carIsWriteIndex row `shouldBe` Nothing
Left e -> expectationFailure ("expected parse, got " <> e)
it "decodes \"false\" for is_write_index as Just False" $
case parseEither parseJSON =<< eitherDecode falseWriteIndexRow :: Either String CatAliasesRow of
Right row -> carIsWriteIndex row `shouldBe` Just False
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates a native JSON bool for is_write_index" $
case parseEither parseJSON =<< eitherDecode nativeBoolRow :: Either String CatAliasesRow of
Right row -> carIsWriteIndex row `shouldBe` Just True
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in carOther" $
case parseEither parseJSON =<< eitherDecode extraColumnRow :: Either String CatAliasesRow of
Right row ->
case carOther row of
Object o -> KM.keys o `shouldContain` ["alias", "index", "ts"]
_ -> expectationFailure "expected carOther to be an Object"
Left e -> expectationFailure ("expected parse, got " <> e)
it "rejects an unrecognised is_write_index string" $
case parseEither parseJSON =<< eitherDecode badWriteIndexRow :: Either String CatAliasesRow of
Right row -> expectationFailure ("expected parse failure, got " <> show row)
Left _ -> return ()
-- ------------------------------------------------------------------ --
-- catAliasesWith integration (live ES) --
-- ------------------------------------------------------------------ --
describe "catAliasesWith integration" $ do
it "returns a row for an alias we just created"
$ withTestEnv
$ withCatAlias
[qqIndexName|bloodhound-tests-cat-aliases-src|]
[qqIndexName|bloodhound-tests-cat-alias|]
$ \idx alias -> do
rows <- Client.catAliases
liftIO $
case filter ((== alias) . carAlias) rows of
(row : _) -> do
carAlias row `shouldBe` alias
carIndex row `shouldBe` idx
carIsWriteIndex row `shouldSatisfy` isJust
[] ->
expectationFailure "expected at least one row for the freshly-created alias"
it "filters by alias name via the /<alias> path segment"
$ withTestEnv
$ withCatAlias
[qqIndexName|bloodhound-tests-cat-aliases-filter-src|]
[qqIndexName|bloodhound-tests-cat-alias-filter|]
$ \_idx alias -> do
rows <- Client.catAliasesWith (Just (AliasName alias)) defaultCatAliasesOptions
liftIO $ do
-- At least one row matches the filter, and every returned
-- row points at our alias (the path-segment filter narrows
-- the result to a single name).
rows `shouldSatisfy` not . null
map carAlias rows `shouldSatisfy` all (== alias)
it "honours the h= parameter and leaves unrequested columns absent"
$ withTestEnv
$ withCatAlias
[qqIndexName|bloodhound-tests-cat-aliases-col-src|]
[qqIndexName|bloodhound-tests-cat-alias-col|]
$ \idx alias -> do
let opts =
defaultCatAliasesOptions
{ caaColumns = Just (CatAliasColAlias :| [CatAliasColIndex])
}
rows <- Client.catAliasesWith (Just (AliasName alias)) opts
liftIO $
case filter ((== alias) . carAlias) rows of
(row : _) -> do
-- Requested columns come back populated:
carAlias row `shouldBe` alias
carIndex row `shouldBe` idx
-- Columns we did not request should be absent:
carFilter row `shouldBe` Nothing
carRoutingIndex row `shouldBe` Nothing
carRoutingSearch row `shouldBe` Nothing
[] ->
expectationFailure "expected at least one row for the freshly-created alias"
-- ------------------------------------------------------------------ --
-- catAllocationOptionsParams: pure URI rendering (no ES required) --
-- ------------------------------------------------------------------ --
describe "catAllocationOptionsParams URI rendering" $ do
it "defaultCatAllocationOptions emits only format=json" $
catAllocationOptionsParams defaultCatAllocationOptions
`shouldBe` [("format", Just "json")]
it "renders bytes via catBytesSizeText" $ do
let opts = defaultCatAllocationOptions {caloBytes = Just CatBytesKb}
catAllocationOptionsParams opts
`shouldContain` [("bytes", Just "kb")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatAllocationOptions
{ caloColumns = Just (CatAllocColNode :| [CatAllocColShards, CatAllocColDiskUsed])
}
catAllocationOptionsParams opts
`shouldContain` [("h", Just "node,shards,disk.used")]
it "renders unknown columns verbatim" $ do
let opts =
defaultCatAllocationOptions
{ caloColumns = Just (CatAllocColOther "di" :| [])
}
catAllocationOptionsParams opts
`shouldContain` [("h", Just "di")]
it "renders expand_wildcards as a comma-joined list" $ do
let opts =
defaultCatAllocationOptions
{ caloExpandWildcards = Just (ExpandWildcardsOpen :| [ExpandWildcardsClosed])
}
catAllocationOptionsParams opts
`shouldContain` [("expand_wildcards", Just "open,closed")]
it "renders booleans as true/false" $ do
let opts =
defaultCatAllocationOptions
{ caloHelp = Just True,
caloLocal = Just False,
caloVerbose = Just True
}
let ps = catAllocationOptionsParams opts
ps `shouldContain` [("help", Just "true")]
ps `shouldContain` [("local", Just "false")]
ps `shouldContain` [("v", Just "true")]
it "renders master_timeout via timeUnitsSuffix" $ do
let opts = defaultCatAllocationOptions {caloMasterTimeout = Just (TimeUnitSeconds, 30)}
catAllocationOptionsParams opts
`shouldContain` [("master_timeout", Just "30s")]
it "renders sort specs as comma-joined column[:direction] terms" $ do
let opts =
defaultCatAllocationOptions
{ caloSort =
Just
( CatAllocationSortSpec CatAllocColShards (Just CatSortDesc)
:| [CatAllocationSortSpec CatAllocColNode Nothing]
)
}
catAllocationOptionsParams opts
`shouldContain` [("s", Just "shards:desc,node")]
-- ------------------------------------------------------------------ --
-- Renderer sanity for the leaf sum types --
-- ------------------------------------------------------------------ --
describe "catAllocationColumnText rendering" $
it "covers every documented column" $
map
catAllocationColumnText
[ CatAllocColShards,
CatAllocColShardsUndesired,
CatAllocColWriteLoadForecast,
CatAllocColDiskIndicesForecast,
CatAllocColDiskIndices,
CatAllocColDiskUsed,
CatAllocColDiskAvail,
CatAllocColDiskTotal,
CatAllocColDiskPercent,
CatAllocColHost,
CatAllocColIp,
CatAllocColNode,
CatAllocColNodeRole
]
`shouldBe` [ "shards",
"shards.undesired",
"write_load.forecast",
"disk.indices.forecast",
"disk.indices",
"disk.used",
"disk.avail",
"disk.total",
"disk.percent",
"host",
"ip",
"node",
"node.role"
]
describe "catShardsUndesiredFromText / ToText round-trip" $ do
it "decodes \"-1\" as NotApplicable" $
catShardsUndesiredFromText "-1" `shouldBe` Just CatShardsUndesiredNotApplicable
it "decodes non-negative integers as Count" $ do
catShardsUndesiredFromText "0" `shouldBe` Just (CatShardsUndesiredCount 0)
catShardsUndesiredFromText "42" `shouldBe` Just (CatShardsUndesiredCount 42)
it "rejects non-sentinel negative values and garbage" $ do
catShardsUndesiredFromText "-2" `shouldBe` (Nothing :: Maybe CatShardsUndesired)
catShardsUndesiredFromText "abc" `shouldBe` (Nothing :: Maybe CatShardsUndesired)
it "round-trips every constructor through ToText" $ do
catShardsUndesiredToText CatShardsUndesiredNotApplicable `shouldBe` "-1"
catShardsUndesiredToText (CatShardsUndesiredCount 7) `shouldBe` "7"
-- ------------------------------------------------------------------ --
-- CatAllocationRow JSON decoding --
-- ------------------------------------------------------------------ --
describe "CatAllocationRow FromJSON" $ do
let fullRow =
"{\
\ \"shards\":\"1\",\
\ \"shards.undesired\":\"0\",\
\ \"write_load.forecast\":\"0.0\",\
\ \"disk.indices.forecast\":\"260b\",\
\ \"disk.indices\":\"260b\",\
\ \"disk.used\":\"47.3gb\",\
\ \"disk.avail\":\"43.4gb\",\
\ \"disk.total\":\"100.7gb\",\
\ \"disk.percent\":\"46\",\
\ \"host\":\"127.0.0.1\",\
\ \"ip\":\"127.0.0.1\",\
\ \"node\":\"CSUXak2\",\
\ \"node.role\":\"himrst\"\
\}"
minimalRow =
"{\
\ \"node\":\"bloodhound-tests-cat-node\"\
\}"
subsetRow =
"{\
\ \"node\":\"bloodhound-tests-cat-node\",\
\ \"shards\":\"4\",\
\ \"disk.indices\":\"100kb\"\
\}"
sentinelRow =
"{\
\ \"node\":\"bloodhound-tests-cat-node\",\
\ \"shards.undesired\":\"-1\"\
\}"
fractionalRow =
"{\
\ \"node\":\"bloodhound-tests-cat-node\",\
\ \"disk.used\":\"5.2gb\",\
\ \"disk.avail\":\"5.2gb\"\
\}"
bareRow =
"{\
\ \"node\":\"bloodhound-tests-cat-node\",\
\ \"disk.used\":\"208\",\
\ \"disk.avail\":\"208\"\
\}"
extraColumnRow =
"{\
\ \"node\":\"bloodhound-tests-cat-node\",\
\ \"ragney.future\":\"xyz\"\
\}"
badUnitRow =
"{\
\ \"node\":\"bloodhound-tests-cat-node\",\
\ \"disk.used\":\"5qb\"\
\}"
it "parses a fully-populated default row" $
case parseEither parseJSON =<< eitherDecode fullRow :: Either String CatAllocationRow of
Right row -> do
calrShards row `shouldBe` Just 1
calrShardsUndesired row `shouldBe` Just (CatShardsUndesiredCount 0)
calrWriteLoadForecast row `shouldBe` Just 0.0
calrDiskIndicesForecast row `shouldBe` Just (Bytes 260, CatBytesBytes)
calrDiskIndices row `shouldBe` Just (Bytes 260, CatBytesBytes)
calrDiskUsed row `shouldBe` Just (Bytes 47, CatBytesGb)
calrDiskAvail row `shouldBe` Just (Bytes 43, CatBytesGb)
calrDiskTotal row `shouldBe` Just (Bytes 101, CatBytesGb)
calrDiskPercent row `shouldBe` Just 46
calrHost row `shouldBe` Just "127.0.0.1"
calrIp row `shouldBe` Just "127.0.0.1"
calrNode row `shouldBe` "CSUXak2"
calrNodeRole row `shouldBe` Just "himrst"
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses a minimal row containing only `node`" $
case parseEither parseJSON =<< eitherDecode minimalRow :: Either String CatAllocationRow of
Right row -> do
calrNode row `shouldBe` "bloodhound-tests-cat-node"
calrShards row `shouldBe` Nothing
calrDiskUsed row `shouldBe` Nothing
calrDiskPercent row `shouldBe` Nothing
calrNodeRole row `shouldBe` Nothing
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses a column-subset row (as produced by h=node,shards,disk.indices)" $
case parseEither parseJSON =<< eitherDecode subsetRow :: Either String CatAllocationRow of
Right row -> do
calrNode row `shouldBe` "bloodhound-tests-cat-node"
calrShards row `shouldBe` Just 4
calrDiskIndices row `shouldBe` Just (Bytes 100, CatBytesKb)
calrDiskUsed row `shouldBe` Nothing
Left e -> expectationFailure ("expected parse, got " <> e)
it "decodes the \"-1\" sentinel for shards.undesired as NotApplicable" $
case parseEither parseJSON =<< eitherDecode sentinelRow :: Either String CatAllocationRow of
Right row -> calrShardsUndesired row `shouldBe` Just CatShardsUndesiredNotApplicable
Left e -> expectationFailure ("expected parse, got " <> e)
it "rounds fractional disk magnitudes to the nearest whole byte" $
case parseEither parseJSON =<< eitherDecode fractionalRow :: Either String CatAllocationRow of
Right row -> do
calrDiskUsed row `shouldBe` Just (Bytes 5, CatBytesGb)
calrDiskAvail row `shouldBe` Just (Bytes 5, CatBytesGb)
Left e -> expectationFailure ("expected parse, got " <> e)
it "treats a bare number as CatBytesBytes" $
case parseEither parseJSON =<< eitherDecode bareRow :: Either String CatAllocationRow of
Right row -> do
calrDiskUsed row `shouldBe` Just (Bytes 208, CatBytesBytes)
calrDiskAvail row `shouldBe` Just (Bytes 208, CatBytesBytes)
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in calrOther" $
case parseEither parseJSON =<< eitherDecode extraColumnRow :: Either String CatAllocationRow of
Right row ->
case calrOther row of
Object o -> KM.keys o `shouldContain` ["node", "ragney.future"]
_ -> expectationFailure "expected calrOther to be an Object"
Left e -> expectationFailure ("expected parse, got " <> e)
it "rejects an unrecognised byte-size suffix" $
case parseEither parseJSON =<< eitherDecode badUnitRow :: Either String CatAllocationRow of
Right row -> expectationFailure ("expected parse failure, got " <> show row)
Left _ -> return ()
-- Defensive coverage: native JSON numbers should be tolerated for
-- the integer columns, matching the CatIndicesRow precedent.
it "tolerates a native JSON number for an integer column (shards)" $
case parseEither parseJSON =<< eitherDecode "{\"node\":\"x\",\"shards\":7}" :: Either String CatAllocationRow of
Right row -> calrShards row `shouldBe` Just 7
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates a native JSON number for disk.percent" $
case parseEither parseJSON =<< eitherDecode "{\"node\":\"x\",\"disk.percent\":50}" :: Either String CatAllocationRow of
Right row -> calrDiskPercent row `shouldBe` Just 50
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates a native JSON number for disk.used (treated as bare bytes)" $
case parseEither parseJSON =<< eitherDecode "{\"node\":\"x\",\"disk.used\":208}" :: Either String CatAllocationRow of
Right row -> calrDiskUsed row `shouldBe` Just (Bytes 208, CatBytesBytes)
Left e -> expectationFailure ("expected parse, got " <> e)
-- Real-world shape: ES emits a synthetic "UNASSIGNED" pseudo-row
-- for unassigned shards, with the disk columns null. Locks in that
-- the parser tolerates the null disk values and preserves the
-- documented pseudo-node name verbatim.
it "parses the UNASSIGNED pseudo-node row with null disk columns" $
case parseEither parseJSON =<< eitherDecode "{\"node\":\"UNASSIGNED\",\"shards\":\"3\",\"disk.percent\":null}" :: Either String CatAllocationRow of
Right row -> do
calrNode row `shouldBe` "UNASSIGNED"
calrShards row `shouldBe` Just 3
calrDiskPercent row `shouldBe` Nothing
calrDiskUsed row `shouldBe` Nothing
calrDiskIndices row `shouldBe` Nothing
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates a native JSON -1 for shards.undesired as NotApplicable" $
case parseEither parseJSON =<< eitherDecode "{\"node\":\"x\",\"shards.undesired\":-1}" :: Either String CatAllocationRow of
Right row -> calrShardsUndesired row `shouldBe` Just CatShardsUndesiredNotApplicable
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates a native JSON number for write_load.forecast (fractional)" $
case parseEither parseJSON =<< eitherDecode "{\"node\":\"x\",\"write_load.forecast\":1.5}" :: Either String CatAllocationRow of
Right row -> calrWriteLoadForecast row `shouldBe` Just 1.5
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catAllocationWith integration (live ES) --
-- ------------------------------------------------------------------ --
describe "catAllocationWith integration" $ do
it "returns at least one row after creating an index" $
withTestEnv $
withCatIndex [qqIndexName|bloodhound-tests-cat-allocation|] $
\_idx -> do
rows <- Client.catAllocation
liftIO $
rows `shouldSatisfy` (not . null)
it "filters by node name via the /<node_id> path segment" $
withTestEnv $
withCatIndex [qqIndexName|bloodhound-tests-cat-allocation-filter|] $
\_idx -> do
allRows <- Client.catAllocation
-- Exclude the synthetic UNASSIGNED pseudo-row, which has no
-- real node name to filter by and would produce an empty
-- (and confusing) result if pinned.
case filter (\r -> calrNode r /= "UNASSIGNED") allRows of
[] -> liftIO $ expectationFailure "expected at least one data node row"
(firstRow : _) -> do
let name = calrNode firstRow
filtered <- Client.catAllocationWith (Just (NodeName name)) defaultCatAllocationOptions
-- OpenSearch's /_cat/allocation/<node_id> includes the
-- synthetic UNASSIGNED pseudo-row even when filtering by
-- node id (ES does not); strip it before asserting so
-- the test passes on both families.
let realFiltered = filter (\r -> calrNode r /= "UNASSIGNED") filtered
liftIO $ do
realFiltered `shouldSatisfy` (not . null)
map calrNode realFiltered `shouldSatisfy` all (== name)
-- ------------------------------------------------------------------ --
-- catCountOptionsParams: pure URI rendering (no ES required) --
-- ------------------------------------------------------------------ --
describe "catCountOptionsParams URI rendering" $ do
it "defaultCatCountOptions emits only format=json" $
catCountOptionsParams defaultCatCountOptions
`shouldBe` [("format", Just "json")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatCountOptions
{ ccoColumns = Just (CatCountColEpoch :| [CatCountColCount])
}
catCountOptionsParams opts
`shouldContain` [("h", Just "epoch,count")]
it "renders unknown columns verbatim" $ do
let opts =
defaultCatCountOptions
{ ccoColumns = Just (CatCountColOther "t" :| [])
}
catCountOptionsParams opts
`shouldContain` [("h", Just "t")]
it "renders booleans as true/false" $ do
let opts =
defaultCatCountOptions
{ ccoHelp = Just True,
ccoLocal = Just False,
ccoVerbose = Just True
}
let ps = catCountOptionsParams opts
ps `shouldContain` [("help", Just "true")]
ps `shouldContain` [("local", Just "false")]
ps `shouldContain` [("v", Just "true")]
it "renders master_timeout via timeUnitsSuffix" $ do
let opts = defaultCatCountOptions {ccoMasterTimeout = Just (TimeUnitSeconds, 30)}
catCountOptionsParams opts
`shouldContain` [("master_timeout", Just "30s")]
describe "catCountColumnText rendering" $
it "covers every documented column" $
map
catCountColumnText
[CatCountColEpoch, CatCountColTimestamp, CatCountColCount]
`shouldBe` ["epoch", "timestamp", "count"]
-- ------------------------------------------------------------------ --
-- CatCountRow JSON decoding --
-- ------------------------------------------------------------------ --
describe "CatCountRow FromJSON" $ do
let fullRow =
"[{\"epoch\":\"1782112550\",\"timestamp\":\"07:15:50\",\"count\":\"1217\"}]"
minimalRow = "[{\"count\":\"0\"}]"
extraColumnRow = "[{\"count\":\"5\",\"ragney.future\":\"xyz\"}]"
it "parses a fully-populated default row" $
case parseEither parseJSON =<< eitherDecode fullRow :: Either String [CatCountRow] of
Right (row : _) -> do
ccrEpoch row `shouldBe` Just 1782112550
ccrTimestamp row `shouldBe` Just "07:15:50"
ccrCount row `shouldBe` Just 1217
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses a minimal row containing only count" $
case parseEither parseJSON =<< eitherDecode minimalRow :: Either String [CatCountRow] of
Right (row : _) -> ccrCount row `shouldBe` Just 0
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in ccrOther" $
case parseEither parseJSON =<< eitherDecode extraColumnRow :: Either String [CatCountRow] of
Right (row : _) ->
case ccrOther row of
Object o -> KM.keys o `shouldContain` ["count", "ragney.future"]
_ -> expectationFailure "expected ccrOther to be an Object"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates a native JSON number for count" $
case parseEither parseJSON =<< eitherDecode "[{\"count\":42}]" :: Either String [CatCountRow] of
Right (row : _) -> ccrCount row `shouldBe` Just 42
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates a native JSON number for epoch" $
case parseEither parseJSON =<< eitherDecode "[{\"epoch\":1782112550}]" :: Either String [CatCountRow] of
Right (row : _) -> ccrEpoch row `shouldBe` Just 1782112550
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catCountWith integration (live ES) --
-- ------------------------------------------------------------------ --
describe "catCountWith integration" $
it "returns a count row after creating an index" $
withTestEnv $
withCatIndex [qqIndexName|bloodhound-tests-cat-count|] $
\_idx -> do
rows <- Client.catCount
liftIO $
rows `shouldSatisfy` (not . null)
-- ------------------------------------------------------------------ --
-- catMasterOptionsParams: pure URI rendering (no ES required) --
-- ------------------------------------------------------------------ --
describe "catMasterOptionsParams URI rendering" $ do
it "defaultCatMasterOptions emits only format=json" $
catMasterOptionsParams defaultCatMasterOptions
`shouldBe` [("format", Just "json")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatMasterOptions
{ cmoColumns = Just (CatMasterColId :| [CatMasterColNode])
}
catMasterOptionsParams opts
`shouldContain` [("h", Just "id,node")]
it "renders booleans as true/false" $ do
let opts =
defaultCatMasterOptions
{ cmoHelp = Just True,
cmoLocal = Just True
}
let ps = catMasterOptionsParams opts
ps `shouldContain` [("help", Just "true")]
ps `shouldContain` [("local", Just "true")]
it "renders master_timeout via timeUnitsSuffix" $ do
let opts = defaultCatMasterOptions {cmoMasterTimeout = Just (TimeUnitSeconds, 30)}
catMasterOptionsParams opts
`shouldContain` [("master_timeout", Just "30s")]
describe "catMasterColumnText rendering" $
it "covers every documented column" $
map
catMasterColumnText
[CatMasterColId, CatMasterColHost, CatMasterColIp, CatMasterColNode]
`shouldBe` ["id", "host", "ip", "node"]
-- ------------------------------------------------------------------ --
-- CatMasterRow JSON decoding --
-- ------------------------------------------------------------------ --
describe "CatMasterRow FromJSON" $ do
let fullRow =
"[{\"id\":\"CBH3C4RiQcK3CxFvPJyFtQ\",\"host\":\"172.19.0.7\",\"ip\":\"172.19.0.7\",\"node\":\"elasticsearch1\"}]"
minimalRow = "[{\"node\":\"bloodhound-tests-cat-node\"}]"
extraColumnRow = "[{\"node\":\"x\",\"ragney.future\":\"xyz\"}]"
it "parses a fully-populated default row" $
case parseEither parseJSON =<< eitherDecode fullRow :: Either String [CatMasterRow] of
Right (row : _) -> do
cmrId row `shouldBe` Just (FullNodeId "CBH3C4RiQcK3CxFvPJyFtQ")
cmrHost row `shouldBe` Just "172.19.0.7"
cmrIp row `shouldBe` Just "172.19.0.7"
cmrNode row `shouldBe` Just "elasticsearch1"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses a minimal row containing only node" $
case parseEither parseJSON =<< eitherDecode minimalRow :: Either String [CatMasterRow] of
Right (row : _) -> do
cmrNode row `shouldBe` Just "bloodhound-tests-cat-node"
cmrId row `shouldBe` Nothing
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in cmrOther" $
case parseEither parseJSON =<< eitherDecode extraColumnRow :: Either String [CatMasterRow] of
Right (row : _) ->
case cmrOther row of
Object o -> KM.keys o `shouldContain` ["node", "ragney.future"]
_ -> expectationFailure "expected cmrOther to be an Object"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catMasterWith integration (live ES) --
-- ------------------------------------------------------------------ --
describe "catMasterWith integration" $
it "returns at least one master row" $
withTestEnv $ do
rows <- Client.catMaster
liftIO $
rows `shouldSatisfy` (not . null)
-- ------------------------------------------------------------------ --
-- catHealthOptionsParams: pure URI rendering (no ES required) --
-- ------------------------------------------------------------------ --
describe "catHealthOptionsParams URI rendering" $ do
it "defaultCatHealthOptions emits only format=json" $
catHealthOptionsParams defaultCatHealthOptions
`shouldBe` [("format", Just "json")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatHealthOptions
{ chtoColumns = Just (CatHealthColCluster :| [CatHealthColStatus])
}
catHealthOptionsParams opts
`shouldContain` [("h", Just "cluster,status")]
it "renders booleans as true/false" $ do
let opts =
defaultCatHealthOptions
{ chtoHelp = Just True,
chtoLocal = Just False,
chtoTs = Just True,
chtoVerbose = Just True
}
let ps = catHealthOptionsParams opts
ps `shouldContain` [("help", Just "true")]
ps `shouldContain` [("local", Just "false")]
ps `shouldContain` [("ts", Just "true")]
ps `shouldContain` [("v", Just "true")]
it "renders master_timeout via timeUnitsSuffix" $ do
let opts = defaultCatHealthOptions {chtoMasterTimeout = Just (TimeUnitSeconds, 30)}
catHealthOptionsParams opts
`shouldContain` [("master_timeout", Just "30s")]
describe "catHealthColumnText rendering" $
it "covers every documented column" $
map
catHealthColumnText
[ CatHealthColEpoch,
CatHealthColTimestamp,
CatHealthColCluster,
CatHealthColStatus,
CatHealthColNodeTotal,
CatHealthColNodeData,
CatHealthColShards,
CatHealthColPri,
CatHealthColRelo,
CatHealthColInit,
CatHealthColUnassign,
CatHealthColPendingTasks,
CatHealthColMaxTaskWaitTime,
CatHealthColActiveShardsPercent
]
`shouldBe` [ "epoch",
"timestamp",
"cluster",
"status",
"node.total",
"node.data",
"shards",
"pri",
"relo",
"init",
"unassign",
"pending_tasks",
"max_task_wait_time",
"active_shards_percent"
]
describe "catHealthStatusFromText / ToText round-trip" $ do
it "decodes green/yellow/red" $ do
catHealthStatusFromText "green" `shouldBe` Just CatHealthStatusGreen
catHealthStatusFromText "yellow" `shouldBe` Just CatHealthStatusYellow
catHealthStatusFromText "red" `shouldBe` Just CatHealthStatusRed
it "rejects unknown values" $
catHealthStatusFromText "purple" `shouldBe` (Nothing :: Maybe CatHealthStatus)
it "round-trips every constructor through ToText" $ do
catHealthStatusText CatHealthStatusGreen `shouldBe` "green"
catHealthStatusText CatHealthStatusYellow `shouldBe` "yellow"
catHealthStatusText CatHealthStatusRed `shouldBe` "red"
-- ------------------------------------------------------------------ --
-- CatHealthRow JSON decoding --
-- ------------------------------------------------------------------ --
describe "CatHealthRow FromJSON" $ do
let fullRow =
"[{\"epoch\":\"1782112550\",\"timestamp\":\"07:15:50\",\"cluster\":\"docker-cluster\",\"status\":\"green\",\"node.total\":\"1\",\"node.data\":\"1\",\"shards\":\"14\",\"pri\":\"14\",\"relo\":\"0\",\"init\":\"0\",\"unassign\":\"0\",\"pending_tasks\":\"0\",\"max_task_wait_time\":\"-\",\"active_shards_percent\":\"100.0%\"}]"
minimalRow = "[{\"cluster\":\"bloodhound-tests\"}]"
extraColumnRow = "[{\"cluster\":\"x\",\"ragney.future\":\"xyz\"}]"
it "parses a fully-populated default row" $
case parseEither parseJSON =<< eitherDecode fullRow :: Either String [CatHealthRow] of
Right (row : _) -> do
chrEpoch row `shouldBe` Just 1782112550
chrTimestamp row `shouldBe` Just "07:15:50"
chrCluster row `shouldBe` Just "docker-cluster"
chrStatus row `shouldBe` Just CatHealthStatusGreen
chrNodeTotal row `shouldBe` Just 1
chrNodeData row `shouldBe` Just 1
chrShards row `shouldBe` Just 14
chrPri row `shouldBe` Just 14
chrRelo row `shouldBe` Just 0
chrInit row `shouldBe` Just 0
chrUnassign row `shouldBe` Just 0
chrPendingTasks row `shouldBe` Just 0
chrMaxTaskWaitTime row `shouldBe` Just "-"
chrActiveShardsPercent row `shouldBe` Just "100.0%"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses a minimal row containing only cluster" $
case parseEither parseJSON =<< eitherDecode minimalRow :: Either String [CatHealthRow] of
Right (row : _) -> do
chrCluster row `shouldBe` Just "bloodhound-tests"
chrStatus row `shouldBe` Nothing
chrShards row `shouldBe` Nothing
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in chrOther" $
case parseEither parseJSON =<< eitherDecode extraColumnRow :: Either String [CatHealthRow] of
Right (row : _) ->
case chrOther row of
Object o -> KM.keys o `shouldContain` ["cluster", "ragney.future"]
_ -> expectationFailure "expected chrOther to be an Object"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates a native JSON number for shards" $
case parseEither parseJSON =<< eitherDecode "[{\"cluster\":\"x\",\"shards\":14}]" :: Either String [CatHealthRow] of
Right (row : _) -> chrShards row `shouldBe` Just 14
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "rejects an unrecognised status value" $
case parseEither parseJSON =<< eitherDecode "[{\"cluster\":\"x\",\"status\":\"purple\"}]" :: Either String [CatHealthRow] of
Right _ -> expectationFailure "expected parse failure"
Left _ -> return ()
-- ------------------------------------------------------------------ --
-- catHealthWith integration (live ES) --
-- ------------------------------------------------------------------ --
describe "catHealthWith integration" $
it "returns at least one health row" $
withTestEnv $ do
rows <- Client.catHealth
liftIO $
rows `shouldSatisfy` (not . null)
-- ------------------------------------------------------------------ --
-- catPendingTasksOptionsParams: pure URI rendering (no ES required) --
-- ------------------------------------------------------------------ --
describe "catPendingTasksOptionsParams URI rendering" $ do
it "defaultCatPendingTasksOptions emits only format=json" $
catPendingTasksOptionsParams defaultCatPendingTasksOptions
`shouldBe` [("format", Just "json")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatPendingTasksOptions
{ cptoColumns = Just (CatPendingTasksColPriority :| [CatPendingTasksColSource])
}
catPendingTasksOptionsParams opts
`shouldContain` [("h", Just "priority,source")]
it "renders booleans as true/false" $ do
let opts =
defaultCatPendingTasksOptions
{ cptoHelp = Just True,
cptoLocal = Just True
}
let ps = catPendingTasksOptionsParams opts
ps `shouldContain` [("help", Just "true")]
ps `shouldContain` [("local", Just "true")]
it "renders master_timeout via timeUnitsSuffix" $ do
let opts = defaultCatPendingTasksOptions {cptoMasterTimeout = Just (TimeUnitSeconds, 30)}
catPendingTasksOptionsParams opts
`shouldContain` [("master_timeout", Just "30s")]
describe "catPendingTasksColumnText rendering" $
it "covers every documented column" $
map
catPendingTasksColumnText
[ CatPendingTasksColInsertOrder,
CatPendingTasksColTimeInQueue,
CatPendingTasksColPriority,
CatPendingTasksColSource
]
`shouldBe` ["insertOrder", "timeInQueue", "priority", "source"]
-- ------------------------------------------------------------------ --
-- CatPendingTasksRow JSON decoding --
-- ------------------------------------------------------------------ --
describe "CatPendingTasksRow FromJSON" $ do
let fullRow =
"[{\"insertOrder\":\"1\",\"timeInQueue\":\"500ms\",\"priority\":\"URGENT\",\"source\":\"create-index [foo]\"}]"
minimalRow = "[{\"source\":\"x\"}]"
extraColumnRow = "[{\"source\":\"x\",\"ragney.future\":\"xyz\"}]"
it "parses a fully-populated default row" $
case parseEither parseJSON =<< eitherDecode fullRow :: Either String [CatPendingTasksRow] of
Right (row : _) -> do
cptrInsertOrder row `shouldBe` Just 1
cptrTimeInQueue row `shouldBe` Just "500ms"
cptrPriority row `shouldBe` Just PendingTaskUrgent
cptrSource row `shouldBe` Just "create-index [foo]"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses a minimal row containing only source" $
case parseEither parseJSON =<< eitherDecode minimalRow :: Either String [CatPendingTasksRow] of
Right (row : _) -> do
cptrSource row `shouldBe` Just "x"
cptrPriority row `shouldBe` Nothing
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in cptrOther" $
case parseEither parseJSON =<< eitherDecode extraColumnRow :: Either String [CatPendingTasksRow] of
Right (row : _) ->
case cptrOther row of
Object o -> do
KM.keys o `shouldContain` ["source"]
KM.keys o `shouldContain` ["ragney.future"]
_ -> expectationFailure "expected cptrOther to be an Object"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates a native JSON number for insertOrder" $
case parseEither parseJSON =<< eitherDecode "[{\"insertOrder\":42}]" :: Either String [CatPendingTasksRow] of
Right (row : _) -> cptrInsertOrder row `shouldBe` Just 42
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses an empty pending-tasks list" $
case parseEither parseJSON =<< eitherDecode "[]" :: Either String [CatPendingTasksRow] of
Right rows -> rows `shouldBe` []
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catPendingTasksWith integration (live ES) --
-- ------------------------------------------------------------------ --
describe "catPendingTasksWith integration" $
it "parses the pending-tasks list without error" $
withTestEnv $ do
_rows <- Client.catPendingTasks
return ()
-- ------------------------------------------------------------------ --
-- catPluginsOptionsParams: pure URI rendering (no ES required) --
-- ------------------------------------------------------------------ --
describe "catPluginsOptionsParams URI rendering" $ do
it "defaultCatPluginsOptions emits only format=json" $
catPluginsOptionsParams defaultCatPluginsOptions
`shouldBe` [("format", Just "json")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatPluginsOptions
{ cpoColumns = Just (CatPluginsColComponent :| [CatPluginsColVersion])
}
catPluginsOptionsParams opts
`shouldContain` [("h", Just "component,version")]
it "renders booleans as true/false" $ do
let opts =
defaultCatPluginsOptions
{ cpoHelp = Just True,
cpoLocal = Just True
}
let ps = catPluginsOptionsParams opts
ps `shouldContain` [("help", Just "true")]
ps `shouldContain` [("local", Just "true")]
it "renders master_timeout via timeUnitsSuffix" $ do
let opts = defaultCatPluginsOptions {cpoMasterTimeout = Just (TimeUnitSeconds, 30)}
catPluginsOptionsParams opts
`shouldContain` [("master_timeout", Just "30s")]
describe "catPluginsColumnText rendering" $
it "covers every documented column" $
map
catPluginsColumnText
[ CatPluginsColId,
CatPluginsColName,
CatPluginsColComponent,
CatPluginsColVersion,
CatPluginsColDescription,
CatPluginsColType
]
`shouldBe` ["id", "name", "component", "version", "description", "type"]
-- ------------------------------------------------------------------ --
-- CatPluginsRow JSON decoding --
-- ------------------------------------------------------------------ --
describe "CatPluginsRow FromJSON" $ do
let fullRow =
"[{\"id\":\"CBH3C4RiQcK3CxFvPJyFtQ\",\"name\":\"elasticsearch1\",\"component\":\"analysis-icu\",\"version\":\"7.17.25\",\"description\":\"The ICU Analysis plugin\",\"type\":\"isolated\"}]"
minimalRow = "[{\"component\":\"x\"}]"
extraColumnRow = "[{\"component\":\"x\",\"ragney.future\":\"xyz\"}]"
it "parses a fully-populated default row" $
case parseEither parseJSON =<< eitherDecode fullRow :: Either String [CatPluginsRow] of
Right (row : _) -> do
cprId row `shouldBe` Just (FullNodeId "CBH3C4RiQcK3CxFvPJyFtQ")
cprName row `shouldBe` Just "elasticsearch1"
cprComponent row `shouldBe` Just "analysis-icu"
cprVersion row `shouldBe` Just "7.17.25"
cprDescription row `shouldBe` Just "The ICU Analysis plugin"
cprType row `shouldBe` Just "isolated"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses a minimal row containing only component" $
case parseEither parseJSON =<< eitherDecode minimalRow :: Either String [CatPluginsRow] of
Right (row : _) -> do
cprComponent row `shouldBe` Just "x"
cprId row `shouldBe` Nothing
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in cprOther" $
case parseEither parseJSON =<< eitherDecode extraColumnRow :: Either String [CatPluginsRow] of
Right (row : _) ->
case cprOther row of
Object o -> KM.keys o `shouldContain` ["component", "ragney.future"]
_ -> expectationFailure "expected cprOther to be an Object"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catPluginsWith integration (live ES) --
-- ------------------------------------------------------------------ --
describe "catPluginsWith integration" $
it "parses the plugins list without error" $
withTestEnv $ do
_rows <- Client.catPlugins
return ()
-- ------------------------------------------------------------------ --
-- catTemplatesOptionsParams: pure URI rendering (no ES required) --
-- ------------------------------------------------------------------ --
describe "catTemplatesOptionsParams URI rendering" $ do
it "defaultCatTemplatesOptions emits only format=json" $
catTemplatesOptionsParams defaultCatTemplatesOptions
`shouldBe` [("format", Just "json")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatTemplatesOptions
{ ctmoColumns = Just (CatTemplatesColName :| [CatTemplatesColOrder])
}
catTemplatesOptionsParams opts
`shouldContain` [("h", Just "name,order")]
it "renders booleans as true/false" $ do
let opts =
defaultCatTemplatesOptions
{ ctmoHelp = Just True,
ctmoLocal = Just True
}
let ps = catTemplatesOptionsParams opts
ps `shouldContain` [("help", Just "true")]
ps `shouldContain` [("local", Just "true")]
it "renders master_timeout via timeUnitsSuffix" $ do
let opts = defaultCatTemplatesOptions {ctmoMasterTimeout = Just (TimeUnitSeconds, 30)}
catTemplatesOptionsParams opts
`shouldContain` [("master_timeout", Just "30s")]
describe "catTemplatesColumnText rendering" $
it "covers every documented column" $
map
catTemplatesColumnText
[ CatTemplatesColName,
CatTemplatesColIndexPatterns,
CatTemplatesColOrder,
CatTemplatesColVersion,
CatTemplatesColComposedOf
]
`shouldBe` ["name", "index_patterns", "order", "version", "composed_of"]
-- ------------------------------------------------------------------ --
-- CatTemplatesRow JSON decoding --
-- ------------------------------------------------------------------ --
describe "CatTemplatesRow FromJSON" $ do
let fullRow =
"[{\"name\":\".monitoring-beats\",\"index_patterns\":\"[.monitoring-beats-7-*]\",\"order\":\"0\",\"version\":\"7140099\",\"composed_of\":\"\"}]"
minimalRow = "[{\"name\":\"bloodhound-tests\"}]"
extraColumnRow = "[{\"name\":\"x\",\"ragney.future\":\"xyz\"}]"
it "parses a fully-populated default row" $
case parseEither parseJSON =<< eitherDecode fullRow :: Either String [CatTemplatesRow] of
Right (row : _) -> do
ctprName row `shouldBe` Just ".monitoring-beats"
ctprIndexPatterns row `shouldBe` Just "[.monitoring-beats-7-*]"
ctprOrder row `shouldBe` Just 0
ctprVersion row `shouldBe` Just 7140099
ctprComposedOf row `shouldBe` Just ""
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses a minimal row containing only name" $
case parseEither parseJSON =<< eitherDecode minimalRow :: Either String [CatTemplatesRow] of
Right (row : _) -> do
ctprName row `shouldBe` Just "bloodhound-tests"
ctprOrder row `shouldBe` Nothing
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in ctprOther" $
case parseEither parseJSON =<< eitherDecode extraColumnRow :: Either String [CatTemplatesRow] of
Right (row : _) ->
case ctprOther row of
Object o -> KM.keys o `shouldContain` ["name", "ragney.future"]
_ -> expectationFailure "expected ctprOther to be an Object"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates a native JSON number for order" $
case parseEither parseJSON =<< eitherDecode "[{\"name\":\"x\",\"order\":3}]" :: Either String [CatTemplatesRow] of
Right (row : _) -> ctprOrder row `shouldBe` Just 3
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates a native JSON number for version" $
case parseEither parseJSON =<< eitherDecode "[{\"name\":\"x\",\"version\":7140099}]" :: Either String [CatTemplatesRow] of
Right (row : _) -> ctprVersion row `shouldBe` Just 7140099
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catTemplatesWith integration (live ES) --
-- ------------------------------------------------------------------ --
describe "catTemplatesWith integration" $
it "returns at least one template row" $
withTestEnv $ do
let tplName = TemplateName "bloodhound-tests-cat-templates"
TemplateName tplText = tplName
bracket_
( void $
performBHRequest $
putTemplate
(IndexTemplate [IndexPattern "bloodhound-tests-cat-templates-*"] Nothing (object []))
tplName
)
(void $ tryPerformBHRequest $ deleteTemplate tplName)
$ do
rows <- Client.catTemplates
liftIO $
map ctprName rows `shouldSatisfy` elem (Just tplText)
-- ------------------------------------------------------------------ --
-- catThreadPoolOptionsParams: pure URI rendering (no ES required) --
-- ------------------------------------------------------------------ --
describe "catThreadPoolOptionsParams URI rendering" $ do
it "defaultCatThreadPoolOptions emits only format=json" $
catThreadPoolOptionsParams defaultCatThreadPoolOptions
`shouldBe` [("format", Just "json")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatThreadPoolOptions
{ ctpoColumns = Just (CatThreadPoolColName :| [CatThreadPoolColActive])
}
catThreadPoolOptionsParams opts
`shouldContain` [("h", Just "name,active")]
it "renders booleans as true/false" $ do
let opts =
defaultCatThreadPoolOptions
{ ctpoHelp = Just True,
ctpoLocal = Just True
}
let ps = catThreadPoolOptionsParams opts
ps `shouldContain` [("help", Just "true")]
ps `shouldContain` [("local", Just "true")]
it "renders master_timeout via timeUnitsSuffix" $ do
let opts = defaultCatThreadPoolOptions {ctpoMasterTimeout = Just (TimeUnitSeconds, 30)}
catThreadPoolOptionsParams opts
`shouldContain` [("master_timeout", Just "30s")]
describe "catThreadPoolColumnText rendering" $
it "covers every documented column" $
map
catThreadPoolColumnText
[ CatThreadPoolColNodeName,
CatThreadPoolColNodeId,
CatThreadPoolColEphemeralNodeId,
CatThreadPoolColPid,
CatThreadPoolColHost,
CatThreadPoolColIp,
CatThreadPoolColPort,
CatThreadPoolColName,
CatThreadPoolColType,
CatThreadPoolColActive,
CatThreadPoolColPoolSize,
CatThreadPoolColQueue,
CatThreadPoolColQueueSize,
CatThreadPoolColRejected,
CatThreadPoolColLargest,
CatThreadPoolColCompleted,
CatThreadPoolColCore,
CatThreadPoolColMax,
CatThreadPoolColSize,
CatThreadPoolColKeepAlive
]
`shouldBe` [ "node_name",
"node_id",
"ephemeral_node_id",
"pid",
"host",
"ip",
"port",
"name",
"type",
"active",
"pool_size",
"queue",
"queue_size",
"rejected",
"largest",
"completed",
"core",
"max",
"size",
"keep_alive"
]
-- ------------------------------------------------------------------ --
-- CatThreadPoolRow JSON decoding --
-- ------------------------------------------------------------------ --
describe "CatThreadPoolRow FromJSON" $ do
let fullRow =
"[{\"node_name\":\"elasticsearch1\",\"node_id\":\"CBH3C4RiQcK3CxFvPJyFtQ\",\"ephemeral_node_id\":\"2rK7\",\"pid\":\"8\",\"host\":\"172.19.0.7\",\"ip\":\"172.19.0.7\",\"port\":\"9300\",\"name\":\"analyze\",\"type\":\"fixed\",\"active\":\"0\",\"pool_size\":\"1\",\"queue\":\"0\",\"queue_size\":\"16\",\"rejected\":\"0\",\"largest\":\"1\",\"completed\":\"5\",\"core\":null,\"max\":null,\"size\":\"1\",\"keep_alive\":null}]"
minimalRow = "[{\"name\":\"analyze\"}]"
extraColumnRow = "[{\"name\":\"x\",\"ragney.future\":\"xyz\"}]"
it "parses a fully-populated default row" $
case parseEither parseJSON =<< eitherDecode fullRow :: Either String [CatThreadPoolRow] of
Right (row : _) -> do
ctprlNodeName row `shouldBe` Just "elasticsearch1"
ctprlNodeId row `shouldBe` Just "CBH3C4RiQcK3CxFvPJyFtQ"
ctprlEphemeralNodeId row `shouldBe` Just "2rK7"
ctprlPid row `shouldBe` Just 8
ctprlHost row `shouldBe` Just "172.19.0.7"
ctprlIp row `shouldBe` Just "172.19.0.7"
ctprlPort row `shouldBe` Just 9300
ctprlName row `shouldBe` Just "analyze"
ctprlType row `shouldBe` Just "fixed"
ctprlActive row `shouldBe` Just 0
ctprlPoolSize row `shouldBe` Just 1
ctprlQueue row `shouldBe` Just 0
ctprlQueueSize row `shouldBe` Just 16
ctprlRejected row `shouldBe` Just 0
ctprlLargest row `shouldBe` Just 1
ctprlCompleted row `shouldBe` Just 5
ctprlCore row `shouldBe` Nothing
ctprlMax row `shouldBe` Nothing
ctprlSize row `shouldBe` Just 1
ctprlKeepAlive row `shouldBe` Nothing
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses a minimal row containing only name" $
case parseEither parseJSON =<< eitherDecode minimalRow :: Either String [CatThreadPoolRow] of
Right (row : _) -> do
ctprlName row `shouldBe` Just "analyze"
ctprlActive row `shouldBe` Nothing
ctprlPid row `shouldBe` Nothing
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in ctprlOther" $
case parseEither parseJSON =<< eitherDecode extraColumnRow :: Either String [CatThreadPoolRow] of
Right (row : _) ->
case ctprlOther row of
Object o -> KM.keys o `shouldContain` ["name", "ragney.future"]
_ -> expectationFailure "expected ctprlOther to be an Object"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates a native JSON number for active" $
case parseEither parseJSON =<< eitherDecode "[{\"name\":\"x\",\"active\":3}]" :: Either String [CatThreadPoolRow] of
Right (row : _) -> ctprlActive row `shouldBe` Just 3
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catThreadPoolWith integration (live ES) --
-- ------------------------------------------------------------------ --
describe "catThreadPoolWith integration" $
it "returns at least one thread-pool row" $
withTestEnv $ do
rows <- Client.catThreadPool
liftIO $
rows `shouldSatisfy` (not . null)
-- ------------------------------------------------------------------ --
-- catFielddataOptionsParams: pure URI rendering (no ES required) --
-- ------------------------------------------------------------------ --
describe "catFielddataOptionsParams URI rendering" $ do
it "defaultCatFielddataOptions emits only format=json" $
catFielddataOptionsParams defaultCatFielddataOptions
`shouldBe` [("format", Just "json")]
it "renders bytes via catBytesSizeText" $ do
let opts = defaultCatFielddataOptions {cfdoBytes = Just CatBytesKb}
catFielddataOptionsParams opts
`shouldContain` [("bytes", Just "kb")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatFielddataOptions
{ cfdoColumns = Just (CatFielddataColNode :| [CatFielddataColField, CatFielddataColSize])
}
catFielddataOptionsParams opts
`shouldContain` [("h", Just "node,field,size")]
it "renders unknown columns verbatim" $ do
let opts =
defaultCatFielddataOptions
{ cfdoColumns = Just (CatFielddataColOther "description" :| [])
}
catFielddataOptionsParams opts
`shouldContain` [("h", Just "description")]
it "renders booleans as true/false" $ do
let opts =
defaultCatFielddataOptions
{ cfdoHelp = Just True,
cfdoLocal = Just False,
cfdoVerbose = Just True
}
let ps = catFielddataOptionsParams opts
ps `shouldContain` [("help", Just "true")]
ps `shouldContain` [("local", Just "false")]
ps `shouldContain` [("v", Just "true")]
it "renders master_timeout via timeUnitsSuffix" $ do
let opts = defaultCatFielddataOptions {cfdoMasterTimeout = Just (TimeUnitSeconds, 30)}
catFielddataOptionsParams opts
`shouldContain` [("master_timeout", Just "30s")]
it "renders sort specs as comma-joined column[:direction] terms" $ do
let opts =
defaultCatFielddataOptions
{ cfdoSort =
Just
( CatFielddataSortSpec CatFielddataColSize (Just CatSortDesc)
:| [CatFielddataSortSpec CatFielddataColField Nothing]
)
}
catFielddataOptionsParams opts
`shouldContain` [("s", Just "size:desc,field")]
-- ------------------------------------------------------------------ --
-- Renderer sanity for the leaf sum types --
-- ------------------------------------------------------------------ --
describe "catFielddataColumnText rendering" $
it "covers every documented column" $
map
catFielddataColumnText
[ CatFielddataColId,
CatFielddataColHost,
CatFielddataColIp,
CatFielddataColNode,
CatFielddataColField,
CatFielddataColSize
]
`shouldBe` ["id", "host", "ip", "node", "field", "size"]
-- ------------------------------------------------------------------ --
-- CatFielddataRow JSON decoding --
-- ------------------------------------------------------------------ --
describe "CatFielddataRow FromJSON" $ do
let fullRow =
"{\
\ \"id\":\"Qk6MnTpSQ9CZ-Q\",\
\ \"host\":\"127.0.0.1\",\
\ \"ip\":\"127.0.0.1\",\
\ \"node\":\"bloodhound-tests-node\",\
\ \"field\":\"message\",\
\ \"size\":\"5kb\"\
\}"
minimalRow =
"{\
\ \"node\":\"bloodhound-tests-cat-node\",\
\ \"field\":\"message\"\
\}"
fractionalRow =
"{\
\ \"node\":\"bloodhound-tests-cat-node\",\
\ \"field\":\"message\",\
\ \"size\":\"5.2gb\"\
\}"
bareRow =
"{\
\ \"node\":\"bloodhound-tests-cat-node\",\
\ \"field\":\"message\",\
\ \"size\":\"208\"\
\}"
extraColumnRow =
"{\
\ \"node\":\"bloodhound-tests-cat-node\",\
\ \"field\":\"message\",\
\ \"ragney.future\":\"xyz\"\
\}"
badUnitRow =
"{\
\ \"node\":\"bloodhound-tests-cat-node\",\
\ \"field\":\"message\",\
\ \"size\":\"5qb\"\
\}"
it "parses a fully-populated default row" $
case parseEither parseJSON =<< eitherDecode fullRow :: Either String CatFielddataRow of
Right row -> do
cfdrId row `shouldBe` Just "Qk6MnTpSQ9CZ-Q"
cfdrHost row `shouldBe` Just "127.0.0.1"
cfdrIp row `shouldBe` Just "127.0.0.1"
cfdrNode row `shouldBe` "bloodhound-tests-node"
cfdrField row `shouldBe` "message"
cfdrSize row `shouldBe` Just (Bytes 5, CatBytesKb)
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses a minimal row containing only node and field" $
case parseEither parseJSON =<< eitherDecode minimalRow :: Either String CatFielddataRow of
Right row -> do
cfdrNode row `shouldBe` "bloodhound-tests-cat-node"
cfdrField row `shouldBe` "message"
cfdrSize row `shouldBe` Nothing
cfdrHost row `shouldBe` Nothing
Left e -> expectationFailure ("expected parse, got " <> e)
it "rounds fractional size magnitudes to the nearest whole byte" $
case parseEither parseJSON =<< eitherDecode fractionalRow :: Either String CatFielddataRow of
Right row -> cfdrSize row `shouldBe` Just (Bytes 5, CatBytesGb)
Left e -> expectationFailure ("expected parse, got " <> e)
it "treats a bare number as CatBytesBytes" $
case parseEither parseJSON =<< eitherDecode bareRow :: Either String CatFielddataRow of
Right row -> cfdrSize row `shouldBe` Just (Bytes 208, CatBytesBytes)
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in cfdrOther" $
case parseEither parseJSON =<< eitherDecode extraColumnRow :: Either String CatFielddataRow of
Right row ->
case cfdrOther row of
Object o -> do
KM.keys o `shouldContain` ["ragney.future"]
KM.keys o `shouldContain` ["field"]
KM.keys o `shouldContain` ["node"]
_ -> expectationFailure "expected cfdrOther to be an Object"
Left e -> expectationFailure ("expected parse, got " <> e)
it "rejects an unrecognised byte-size suffix" $
case parseEither parseJSON =<< eitherDecode badUnitRow :: Either String CatFielddataRow of
Right row -> expectationFailure ("expected parse failure, got " <> show row)
Left _ -> return ()
it "tolerates a native JSON number for size (treated as bare bytes)" $
case parseEither parseJSON =<< eitherDecode "{\"node\":\"x\",\"field\":\"f\",\"size\":208}" :: Either String CatFielddataRow of
Right row -> cfdrSize row `shouldBe` Just (Bytes 208, CatBytesBytes)
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catFielddataWith integration (live ES) --
-- ------------------------------------------------------------------ --
describe "catFielddataWith integration" $ do
it "returns a row for a field after loading its fielddata"
$ withTestEnv
$ withCatFielddata
[qqIndexName|bloodhound-tests-cat-fielddata|]
(FieldName "message")
$ \idx field -> do
let loadSearch =
(mkSearch Nothing Nothing)
{ sortBody = Just [DefaultSortSpec (mkSort field Ascending)]
}
_ <- tryPerformBHRequest $ searchByIndex @Value idx loadSearch
rows <- Client.catFielddata
liftIO $
case filter ((== unFieldName field) . cfdrField) rows of
(row : _) -> cfdrField row `shouldBe` unFieldName field
[] ->
expectationFailure "expected a fielddata row for the freshly-loaded field"
it "honours the h= parameter and leaves unrequested columns absent"
$ withTestEnv
$ withCatFielddata
[qqIndexName|bloodhound-tests-cat-fielddata-h|]
(FieldName "message")
$ \idx field -> do
let loadSearch =
(mkSearch Nothing Nothing)
{ sortBody = Just [DefaultSortSpec (mkSort field Ascending)]
}
_ <- tryPerformBHRequest $ searchByIndex @Value idx loadSearch
let opts =
defaultCatFielddataOptions
{ cfdoColumns = Just (CatFielddataColNode :| [CatFielddataColField])
}
rows <- Client.catFielddataWith Nothing opts
liftIO $
case filter ((== "message") . cfdrField) rows of
(row : _) -> do
cfdrField row `shouldBe` "message"
cfdrNode row `shouldNotBe` ""
cfdrSize row `shouldBe` Nothing
[] ->
expectationFailure "expected a fielddata row for the freshly-loaded field"
it "filters by field name via the /<fields> path segment"
$ withTestEnv
$ withCatFielddata
[qqIndexName|bloodhound-tests-cat-fielddata-filter|]
(FieldName "message")
$ \idx field -> do
let loadSearch =
(mkSearch Nothing Nothing)
{ sortBody = Just [DefaultSortSpec (mkSort field Ascending)]
}
_ <- tryPerformBHRequest $ searchByIndex @Value idx loadSearch
rows <- Client.catFielddataWith (Just field) defaultCatFielddataOptions
liftIO $ do
rows `shouldSatisfy` (not . null)
map cfdrField rows `shouldSatisfy` all (== "message")
-- ------------------------------------------------------------------ --
-- catNodeattrsOptionsParams: pure URI rendering (no ES required) --
-- ------------------------------------------------------------------ --
describe "catNodeattrsOptionsParams URI rendering" $ do
it "defaultCatNodeattrsOptions emits only format=json" $
catNodeattrsOptionsParams defaultCatNodeattrsOptions
`shouldBe` [("format", Just "json")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatNodeattrsOptions
{ cnaoColumns = Just (CatNodeattrsColNode :| [CatNodeattrsColAttr, CatNodeattrsColValue])
}
catNodeattrsOptionsParams opts
`shouldContain` [("h", Just "node,attr,value")]
it "renders unknown columns verbatim" $ do
let opts =
defaultCatNodeattrsOptions
{ cnaoColumns = Just (CatNodeattrsColOther "attr.name" :| [])
}
catNodeattrsOptionsParams opts
`shouldContain` [("h", Just "attr.name")]
it "renders booleans as true/false" $ do
let opts =
defaultCatNodeattrsOptions
{ cnaoHelp = Just True,
cnaoLocal = Just False,
cnaoVerbose = Just True
}
let ps = catNodeattrsOptionsParams opts
ps `shouldContain` [("help", Just "true")]
ps `shouldContain` [("local", Just "false")]
ps `shouldContain` [("v", Just "true")]
it "renders master_timeout via timeUnitsSuffix" $ do
let opts = defaultCatNodeattrsOptions {cnaoMasterTimeout = Just (TimeUnitSeconds, 30)}
catNodeattrsOptionsParams opts
`shouldContain` [("master_timeout", Just "30s")]
it "renders sort specs as comma-joined column[:direction] terms" $ do
let opts =
defaultCatNodeattrsOptions
{ cnaoSort =
Just
( CatNodeattrsSortSpec CatNodeattrsColNode (Just CatSortDesc)
:| [CatNodeattrsSortSpec CatNodeattrsColAttr Nothing]
)
}
catNodeattrsOptionsParams opts
`shouldContain` [("s", Just "node:desc,attr")]
-- ------------------------------------------------------------------ --
-- Renderer sanity for the leaf sum types --
-- ------------------------------------------------------------------ --
describe "catNodeattrsColumnText rendering" $
it "covers every documented column" $
map
catNodeattrsColumnText
[ CatNodeattrsColNode,
CatNodeattrsColId,
CatNodeattrsColPid,
CatNodeattrsColHost,
CatNodeattrsColIp,
CatNodeattrsColPort,
CatNodeattrsColAttr,
CatNodeattrsColValue
]
`shouldBe` ["node", "id", "pid", "host", "ip", "port", "attr", "value"]
-- ------------------------------------------------------------------ --
-- CatNodeattrsRow JSON decoding --
-- ------------------------------------------------------------------ --
describe "CatNodeattrsRow FromJSON" $ do
let fullRow =
"[{\"node\":\"elasticsearch1\",\"id\":\"CBH3C4RiQcK3CxFvPJyFtQ\",\"pid\":\"42\",\"host\":\"172.19.0.7\",\"ip\":\"172.19.0.7\",\"port\":\"9300\",\"attr\":\"box_type\",\"value\":\"hot\"}]"
defaultRow =
"[{\"node\":\"elasticsearch1\",\"host\":\"172.19.0.7\",\"ip\":\"172.19.0.7\",\"attr\":\"box_type\",\"value\":\"hot\"}]"
extraColumnRow = "[{\"node\":\"x\",\"attr\":\"a\",\"value\":\"v\",\"ragney.future\":\"xyz\"}]"
it "parses a fully-populated row (all 8 documented columns)" $
case parseEither parseJSON =<< eitherDecode fullRow :: Either String [CatNodeattrsRow] of
Right (row : _) -> do
cnarNode row `shouldBe` Just "elasticsearch1"
cnarId row `shouldBe` Just "CBH3C4RiQcK3CxFvPJyFtQ"
cnarPid row `shouldBe` Just 42
cnarHost row `shouldBe` Just "172.19.0.7"
cnarIp row `shouldBe` Just "172.19.0.7"
cnarPort row `shouldBe` Just 9300
cnarAttr row `shouldBe` Just "box_type"
cnarValue row `shouldBe` Just "hot"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses a default-column row (id/pid/port absent)" $
case parseEither parseJSON =<< eitherDecode defaultRow :: Either String [CatNodeattrsRow] of
Right (row : _) -> do
cnarNode row `shouldBe` Just "elasticsearch1"
cnarHost row `shouldBe` Just "172.19.0.7"
cnarAttr row `shouldBe` Just "box_type"
cnarValue row `shouldBe` Just "hot"
cnarId row `shouldBe` Nothing
cnarPid row `shouldBe` Nothing
cnarPort row `shouldBe` Nothing
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in cnarOther" $
case parseEither parseJSON =<< eitherDecode extraColumnRow :: Either String [CatNodeattrsRow] of
Right (row : _) ->
case cnarOther row of
Object o -> do
KM.keys o `shouldContain` ["node"]
KM.keys o `shouldContain` ["attr"]
KM.keys o `shouldContain` ["value"]
KM.keys o `shouldContain` ["ragney.future"]
_ -> expectationFailure "expected cnarOther to be an Object"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates native JSON numbers for pid and port (some OS builds)" $
case parseEither parseJSON =<< eitherDecode "[{\"node\":\"x\",\"pid\":42,\"port\":9300}]" :: Either String [CatNodeattrsRow] of
Right (row : _) -> do
cnarPid row `shouldBe` Just 42
cnarPort row `shouldBe` Just 9300
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catNodeattrs integration (live ES) --
-- ------------------------------------------------------------------ --
describe "catNodeattrs integration" $
it "returns a (possibly empty) list without throwing" $
withTestEnv $ do
_ <- Client.catNodeattrs
pure ()
-- ------------------------------------------------------------------ --
-- catRepositoriesOptionsParams: pure URI rendering (no ES required) --
-- ------------------------------------------------------------------ --
describe "catRepositoriesOptionsParams URI rendering" $ do
it "defaultCatRepositoriesOptions emits only format=json" $
catRepositoriesOptionsParams defaultCatRepositoriesOptions
`shouldBe` [("format", Just "json")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatRepositoriesOptions
{ creoColumns = Just (CatRepositoriesColId :| [CatRepositoriesColType])
}
catRepositoriesOptionsParams opts
`shouldContain` [("h", Just "id,type")]
it "renders unknown columns verbatim" $ do
let opts =
defaultCatRepositoriesOptions
{ creoColumns = Just (CatRepositoriesColOther "alias" :| [])
}
catRepositoriesOptionsParams opts
`shouldContain` [("h", Just "alias")]
it "renders booleans as true/false" $ do
let opts =
defaultCatRepositoriesOptions
{ creoHelp = Just False,
creoLocal = Just True,
creoVerbose = Just True
}
let ps = catRepositoriesOptionsParams opts
ps `shouldContain` [("help", Just "false")]
ps `shouldContain` [("local", Just "true")]
ps `shouldContain` [("v", Just "true")]
it "renders master_timeout via timeUnitsSuffix" $ do
let opts = defaultCatRepositoriesOptions {creoMasterTimeout = Just (TimeUnitMinutes, 1)}
catRepositoriesOptionsParams opts
`shouldContain` [("master_timeout", Just "1m")]
it "renders sort specs as comma-joined column[:direction] terms" $ do
let opts =
defaultCatRepositoriesOptions
{ creoSort =
Just
( CatRepositoriesSortSpec CatRepositoriesColId (Just CatSortAsc)
:| [CatRepositoriesSortSpec CatRepositoriesColType Nothing]
)
}
catRepositoriesOptionsParams opts
`shouldContain` [("s", Just "id:asc,type")]
-- ------------------------------------------------------------------ --
-- Renderer sanity for the leaf sum types --
-- ------------------------------------------------------------------ --
describe "catRepositoriesColumnText rendering" $
it "covers every documented column" $
map
catRepositoriesColumnText
[CatRepositoriesColId, CatRepositoriesColType]
`shouldBe` ["id", "type"]
-- ------------------------------------------------------------------ --
-- CatRepositoriesRow JSON decoding --
-- ------------------------------------------------------------------ --
describe "CatRepositoriesRow FromJSON" $ do
let multiRow =
"[{\"id\":\"repo1\",\"type\":\"fs\"},{\"id\":\"repo2\",\"type\":\"s3\"}]"
extraColumnRow = "[{\"id\":\"repo1\",\"type\":\"fs\",\"alias\":\"backup\"}]"
it "parses a multi-row response" $
case parseEither parseJSON =<< eitherDecode multiRow :: Either String [CatRepositoriesRow] of
Right rows -> do
rows `shouldSatisfy` ((== 2) . length)
crrId (head rows) `shouldBe` Just "repo1"
crrType (head rows) `shouldBe` Just "fs"
crrId (rows !! 1) `shouldBe` Just "repo2"
crrType (rows !! 1) `shouldBe` Just "s3"
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses an empty response as []" $
case parseEither parseJSON =<< eitherDecode "[]" :: Either String [CatRepositoriesRow] of
Right rows -> rows `shouldBe` []
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in crrOther" $
case parseEither parseJSON =<< eitherDecode extraColumnRow :: Either String [CatRepositoriesRow] of
Right (row : _) ->
case crrOther row of
Object o -> do
KM.keys o `shouldContain` ["id"]
KM.keys o `shouldContain` ["type"]
KM.keys o `shouldContain` ["alias"]
_ -> expectationFailure "expected crrOther to be an Object"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catRepositories integration (live ES) --
-- ------------------------------------------------------------------ --
describe "catRepositories integration" $
it "returns a (possibly empty) list without throwing" $
withTestEnv $ do
_ <- Client.catRepositories
pure ()
-- ------------------------------------------------------------------ --
-- catShardsOptionsParams: pure URI rendering (no ES required) --
-- ------------------------------------------------------------------ --
describe "catShardsOptionsParams URI rendering" $ do
it "defaultCatShardsOptions emits only format=json" $
catShardsOptionsParams defaultCatShardsOptions
`shouldBe` [("format", Just "json")]
it "renders bytes via catBytesSizeText" $ do
let opts = defaultCatShardsOptions {cshoBytes = Just CatBytesKb}
catShardsOptionsParams opts
`shouldContain` [("bytes", Just "kb")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatShardsOptions
{ cshoColumns = Just (CatShardsColIndex :| [CatShardsColShard, CatShardsColNode])
}
catShardsOptionsParams opts
`shouldContain` [("h", Just "index,shard,node")]
it "renders unknown columns verbatim" $ do
let opts =
defaultCatShardsOptions
{ cshoColumns = Just (CatShardsColOther "unassigned.reason" :| [])
}
catShardsOptionsParams opts
`shouldContain` [("h", Just "unassigned.reason")]
it "renders booleans as true/false" $ do
let opts =
defaultCatShardsOptions
{ cshoHelp = Just False,
cshoLocal = Just True,
cshoVerbose = Just True
}
let ps = catShardsOptionsParams opts
ps `shouldContain` [("help", Just "false")]
ps `shouldContain` [("local", Just "true")]
ps `shouldContain` [("v", Just "true")]
it "renders master_timeout via timeUnitsSuffix" $ do
let opts = defaultCatShardsOptions {cshoMasterTimeout = Just (TimeUnitMinutes, 1)}
catShardsOptionsParams opts
`shouldContain` [("master_timeout", Just "1m")]
it "renders time via timeUnitsSuffix" $ do
let opts = defaultCatShardsOptions {cshoTime = Just (TimeUnitSeconds, 5)}
catShardsOptionsParams opts
`shouldContain` [("time", Just "5s")]
it "renders sort specs as comma-joined column[:direction] terms" $ do
let opts =
defaultCatShardsOptions
{ cshoSort =
Just
( CatShardsSortSpec CatShardsColIndex (Just CatSortAsc)
:| [CatShardsSortSpec CatShardsColShard Nothing]
)
}
catShardsOptionsParams opts
`shouldContain` [("s", Just "index:asc,shard")]
-- ------------------------------------------------------------------ --
-- catSegmentsOptionsParams: pure URI rendering (no ES required) --
-- ------------------------------------------------------------------ --
describe "catSegmentsOptionsParams URI rendering" $ do
it "defaultCatSegmentsOptions emits only format=json" $
catSegmentsOptionsParams defaultCatSegmentsOptions
`shouldBe` [("format", Just "json")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatSegmentsOptions
{ csgoColumns = Just (CatSegmentsColIndex :| [CatSegmentsColShard, CatSegmentsColSegment])
}
catSegmentsOptionsParams opts
`shouldContain` [("h", Just "index,shard,segment")]
it "renders unknown columns verbatim" $ do
let opts =
defaultCatSegmentsOptions
{ csgoColumns = Just (CatSegmentsColOther "size" :| [])
}
catSegmentsOptionsParams opts
`shouldContain` [("h", Just "size")]
it "renders bytes via catBytesSizeText" $ do
let opts = defaultCatSegmentsOptions {csgoBytes = Just CatBytesKb}
catSegmentsOptionsParams opts
`shouldContain` [("bytes", Just "kb")]
it "renders booleans as true/false" $ do
let opts =
defaultCatSegmentsOptions
{ csgoHelp = Just False,
csgoLocal = Just True,
csgoVerbose = Just True
}
let ps = catSegmentsOptionsParams opts
ps `shouldContain` [("help", Just "false")]
ps `shouldContain` [("local", Just "true")]
ps `shouldContain` [("v", Just "true")]
it "renders master_timeout via timeUnitsSuffix" $ do
let opts = defaultCatSegmentsOptions {csgoMasterTimeout = Just (TimeUnitMinutes, 1)}
catSegmentsOptionsParams opts
`shouldContain` [("master_timeout", Just "1m")]
it "renders sort specs as comma-joined column[:direction] terms" $ do
let opts =
defaultCatSegmentsOptions
{ csgoSort =
Just
( CatSegmentsSortSpec CatSegmentsColIndex (Just CatSortAsc)
:| [CatSegmentsSortSpec CatSegmentsColShard Nothing]
)
}
catSegmentsOptionsParams opts
`shouldContain` [("s", Just "index:asc,shard")]
-- ------------------------------------------------------------------ --
-- Renderer sanity for the leaf sum types --
-- ------------------------------------------------------------------ --
describe "catShardsColumnText rendering" $
it "covers every documented default column" $
map
catShardsColumnText
[ CatShardsColIndex,
CatShardsColShard,
CatShardsColPrirep,
CatShardsColState,
CatShardsColDocs,
CatShardsColStore,
CatShardsColDataset,
CatShardsColNode
]
`shouldBe` ["index", "shard", "prirep", "state", "docs", "store", "dataset", "node"]
-- ------------------------------------------------------------------ --
-- CatShardsRow JSON decoding --
-- ------------------------------------------------------------------ --
describe "CatShardsRow FromJSON" $ do
let multiRow =
"[{\"index\":\"logs\",\"shard\":\"0\",\"prirep\":\"p\",\"state\":\"STARTED\",\"docs\":\"42\",\"store\":\"5kb\",\"dataset\":\"tsdb\",\"node\":\"node-1\"},\
\{\"index\":\"logs\",\"shard\":\"1\",\"prirep\":\"r\",\"state\":\"STARTED\",\"docs\":\"42\",\"store\":\"5kb\",\"node\":\"node-2\"}]"
unassignedRow =
"[{\"index\":\"logs\",\"shard\":\"0\",\"prirep\":\"p\",\"state\":\"UNASSIGNED\",\"docs\":\"-\",\"store\":\"-\",\"node\":\"-\"}]"
extraColumnRow =
"[{\"index\":\"logs\",\"shard\":\"0\",\"prirep\":\"p\",\"state\":\"STARTED\",\"docs\":\"42\",\"store\":\"5kb\",\"node\":\"node-1\",\"sync_id\":\"abc\"}]"
nativeNumberRow =
"[{\"index\":\"logs\",\"shard\":0,\"prirep\":\"p\",\"state\":\"STARTED\",\"docs\":42,\"store\":\"5kb\",\"node\":\"node-1\"}]"
it "parses a multi-row response" $
case parseEither parseJSON =<< eitherDecode multiRow :: Either String [CatShardsRow] of
Right rows -> do
rows `shouldSatisfy` ((== 2) . length)
let r0 = head rows
csrIndex r0 `shouldBe` Just "logs"
csrShard r0 `shouldBe` Just 0
csrPrirep r0 `shouldBe` Just "p"
csrState r0 `shouldBe` Just "STARTED"
csrDocs r0 `shouldBe` Just 42
csrDataset r0 `shouldBe` Just "tsdb"
csrNode r0 `shouldBe` Just "node-1"
case csrStore r0 of
Just (Bytes n, CatBytesKb) -> n `shouldBe` 5
other -> expectationFailure ("expected 5kb store, got " <> show other)
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses an empty response as []" $
case parseEither parseJSON =<< eitherDecode "[]" :: Either String [CatShardsRow] of
Right rows -> rows `shouldBe` []
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates the \"-\" sentinel for unassigned shards" $
case parseEither parseJSON =<< eitherDecode unassignedRow :: Either String [CatShardsRow] of
Right (row : _) -> do
csrState row `shouldBe` Just "UNASSIGNED"
csrDocs row `shouldBe` Nothing
csrStore row `shouldBe` Nothing
csrNode row `shouldBe` Nothing
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in csrOther" $
case parseEither parseJSON =<< eitherDecode extraColumnRow :: Either String [CatShardsRow] of
Right (row : _) ->
case csrOther row of
Object o -> do
KM.keys o `shouldContain` ["index"]
KM.keys o `shouldContain` ["sync_id"]
_ -> expectationFailure "expected csrOther to be an Object"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates native JSON numbers (OpenSearch parity)" $
case parseEither parseJSON =<< eitherDecode nativeNumberRow :: Either String [CatShardsRow] of
Right (row : _) -> do
csrShard row `shouldBe` Just 0
csrDocs row `shouldBe` Just 42
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catShards integration (live ES) --
-- ------------------------------------------------------------------ --
describe "catShards integration" $ do
it "returns a (possibly empty) list without throwing" $
withTestEnv $ do
_ <- Client.catShards
pure ()
it "narrows by index via catShardsWith" $
withTestEnv $
withCatIndex [qqIndexName|bloodhound-cat-shards-spec|] $ \idx -> do
rows <- Client.catShardsWith (Just idx) defaultCatShardsOptions
liftIO $ do
rows `shouldSatisfy` (not . null)
rows `shouldSatisfy` all ((== Just (unIndexName idx)) . csrIndex)
-- ------------------------------------------------------------------ --
-- catTasksOptionsParams: pure URI rendering (no ES required) --
-- ------------------------------------------------------------------ --
describe "catTasksOptionsParams URI rendering" $ do
it "defaultCatTasksOptions emits only format=json" $
catTasksOptionsParams defaultCatTasksOptions
`shouldBe` [("format", Just "json")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatTasksOptions
{ cttoColumns = Just (CatTasksColAction :| [CatTasksColTaskId])
}
catTasksOptionsParams opts
`shouldContain` [("h", Just "action,task_id")]
it "renders booleans as true/false" $ do
let opts =
defaultCatTasksOptions
{ cttoDetailed = Just True,
cttoHelp = Just True,
cttoLocal = Just True
}
let ps = catTasksOptionsParams opts
ps `shouldContain` [("detailed", Just "true")]
ps `shouldContain` [("help", Just "true")]
ps `shouldContain` [("local", Just "true")]
it "renders master_timeout via timeUnitsSuffix" $ do
let opts = defaultCatTasksOptions {cttoMasterTimeout = Just (TimeUnitSeconds, 30)}
catTasksOptionsParams opts
`shouldContain` [("master_timeout", Just "30s")]
it "renders actions and nodes as comma-joined lists" $ do
let opts =
defaultCatTasksOptions
{ cttoActions = Just ("cluster:monitor/tasks/lists" :| []),
cttoNodes = Just ("node-1" :| ["node-2"])
}
let ps = catTasksOptionsParams opts
ps `shouldContain` [("actions", Just "cluster:monitor/tasks/lists")]
ps `shouldContain` [("nodes", Just "node-1,node-2")]
it "renders parent_task_id verbatim" $ do
let opts = defaultCatTasksOptions {cttoParentTaskId = Just "oTUltX4IQMOUUVeiohTt8A:123"}
catTasksOptionsParams opts
`shouldContain` [("parent_task_id", Just "oTUltX4IQMOUUVeiohTt8A:123")]
it "renders sort specifiers (s) as column[:direction]" $ do
let opts =
defaultCatTasksOptions
{ cttoSort =
Just
( CatTasksSortSpec CatTasksColAction Nothing
:| [CatTasksSortSpec CatTasksColStartTime (Just CatSortDesc)]
)
}
catTasksOptionsParams opts
`shouldContain` [("s", Just "action,start_time:desc")]
describe "catTasksColumnText rendering" $
it "covers every documented column" $
map
catTasksColumnText
[ CatTasksColId,
CatTasksColAction,
CatTasksColTaskId,
CatTasksColParentTaskId,
CatTasksColType,
CatTasksColStartTime,
CatTasksColTimestamp,
CatTasksColRunningTime,
CatTasksColRunningTimeNs,
CatTasksColNodeId,
CatTasksColIp,
CatTasksColPort,
CatTasksColNode,
CatTasksColVersion,
CatTasksColXOpaqueId,
CatTasksColDescription
]
`shouldBe` [ "id",
"action",
"task_id",
"parent_task_id",
"type",
"start_time",
"timestamp",
"running_time",
"running_time_ns",
"node_id",
"ip",
"port",
"node",
"version",
"x_opaque_id",
"description"
]
-- ------------------------------------------------------------------ --
-- CatTasksRow JSON decoding --
-- ------------------------------------------------------------------ --
describe "CatTasksRow FromJSON" $ do
let fullRow =
"[{\"action\":\"cluster:monitor/tasks/lists[n]\",\"task_id\":\"oTUltX4IQMOUUVeiohTt8A:124\",\"parent_task_id\":\"oTUltX4IQMOUUVeiohTt8A:123\",\"type\":\"direct\",\"start_time\":\"1458585884904\",\"timestamp\":\"01:48:24\",\"running_time\":\"44.1micros\",\"ip\":\"127.0.0.1:9300\",\"node\":\"oTUltX4IQMOUUVeiohTt8A\"}]"
minimalRow = "[{\"action\":\"x\"}]"
extraColumnRow = "[{\"action\":\"x\",\"future.column\":\"xyz\"}]"
it "parses a fully-populated default row" $
case parseEither parseJSON =<< eitherDecode fullRow :: Either String [CatTasksRow] of
Right (row : _) -> do
ctrrAction row `shouldBe` Just "cluster:monitor/tasks/lists[n]"
ctrrTaskId row `shouldBe` Just "oTUltX4IQMOUUVeiohTt8A:124"
ctrrParentTaskId row `shouldBe` Just "oTUltX4IQMOUUVeiohTt8A:123"
ctrrType row `shouldBe` Just "direct"
ctrrStartTime row `shouldBe` Just "1458585884904"
ctrrTimestamp row `shouldBe` Just "01:48:24"
ctrrRunningTime row `shouldBe` Just "44.1micros"
ctrrIp row `shouldBe` Just "127.0.0.1:9300"
ctrrNode row `shouldBe` Just "oTUltX4IQMOUUVeiohTt8A"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses a minimal row containing only action" $
case parseEither parseJSON =<< eitherDecode minimalRow :: Either String [CatTasksRow] of
Right (row : _) -> do
ctrrAction row `shouldBe` Just "x"
ctrrTaskId row `shouldBe` Nothing
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in ctrrOther" $
case parseEither parseJSON =<< eitherDecode extraColumnRow :: Either String [CatTasksRow] of
Right (row : _) ->
case ctrrOther row of
Object o -> do
KM.keys o `shouldContain` ["action"]
KM.keys o `shouldContain` ["future.column"]
_ -> expectationFailure "expected ctrrOther to be an Object"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses an empty tasks list" $
case parseEither parseJSON =<< eitherDecode "[]" :: Either String [CatTasksRow] of
Right rows -> rows `shouldBe` []
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catTasksWith integration (live ES) --
-- ------------------------------------------------------------------ --
describe "catTasksWith integration" $
it "parses the tasks list without error" $
withTestEnv $ do
_rows <- Client.catTasks
return ()
-- ------------------------------------------------------------------ --
-- catSnapshotsOptionsParams: pure URI rendering (no ES required) --
-- ------------------------------------------------------------------ --
describe "catSnapshotsOptionsParams URI rendering" $ do
it "defaultCatSnapshotsOptions emits only format=json" $
catSnapshotsOptionsParams defaultCatSnapshotsOptions
`shouldBe` [("format", Just "json")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatSnapshotsOptions
{ csnoColumns = Just (CatSnapshotsColId :| [CatSnapshotsColStatus, CatSnapshotsColRepository])
}
catSnapshotsOptionsParams opts
`shouldContain` [("h", Just "id,status,repository")]
it "renders ignore_unavailable as true/false" $ do
let opts = defaultCatSnapshotsOptions {csnoIgnoreUnavailable = Just True}
catSnapshotsOptionsParams opts
`shouldContain` [("ignore_unavailable", Just "true")]
it "renders booleans (help, local) as true/false" $ do
let opts =
defaultCatSnapshotsOptions
{ csnoHelp = Just True,
csnoLocal = Just True
}
let ps = catSnapshotsOptionsParams opts
ps `shouldContain` [("help", Just "true")]
ps `shouldContain` [("local", Just "true")]
it "renders master_timeout via timeUnitsSuffix" $ do
let opts = defaultCatSnapshotsOptions {csnoMasterTimeout = Just (TimeUnitSeconds, 30)}
catSnapshotsOptionsParams opts
`shouldContain` [("master_timeout", Just "30s")]
it "renders sort specifiers (s) as column[:direction]" $ do
let opts =
defaultCatSnapshotsOptions
{ csnoSort =
Just
( CatSnapshotsSortSpec CatSnapshotsColStartEpoch Nothing
:| [CatSnapshotsSortSpec CatSnapshotsColDuration (Just CatSortDesc)]
)
}
catSnapshotsOptionsParams opts
`shouldContain` [("s", Just "start_epoch,duration:desc")]
describe "catSnapshotsColumnText rendering" $
it "covers every documented column" $
map
catSnapshotsColumnText
[ CatSnapshotsColId,
CatSnapshotsColRepository,
CatSnapshotsColStatus,
CatSnapshotsColStartEpoch,
CatSnapshotsColStartTime,
CatSnapshotsColEndEpoch,
CatSnapshotsColEndTime,
CatSnapshotsColDuration,
CatSnapshotsColIndices,
CatSnapshotsColSuccessfulShards,
CatSnapshotsColFailedShards,
CatSnapshotsColTotalShards,
CatSnapshotsColReason
]
`shouldBe` [ "id",
"repository",
"status",
"start_epoch",
"start_time",
"end_epoch",
"end_time",
"duration",
"indices",
"successful_shards",
"failed_shards",
"total_shards",
"reason"
]
-- ------------------------------------------------------------------ --
-- CatSnapshotsRow JSON decoding --
-- ------------------------------------------------------------------ --
describe "CatSnapshotsRow FromJSON" $ do
let fullRow =
"[{\"id\":\"snap1\",\"repository\":\"repo1\",\"status\":\"FAILED\",\"start_epoch\":\"1445616705\",\"start_time\":\"18:11:45\",\"end_epoch\":\"1445616978\",\"end_time\":\"18:16:18\",\"duration\":\"4.6m\",\"indices\":\"1\",\"successful_shards\":\"4\",\"failed_shards\":\"1\",\"total_shards\":\"5\"}]"
minimalRow = "[{\"id\":\"snap1\"}]"
extraColumnRow = "[{\"id\":\"snap1\",\"future.column\":\"xyz\"}]"
it "parses a fully-populated default row" $
case parseEither parseJSON =<< eitherDecode fullRow :: Either String [CatSnapshotsRow] of
Right (row : _) -> do
csnrId row `shouldBe` Just "snap1"
csnrRepository row `shouldBe` Just "repo1"
csnrStatus row `shouldBe` Just "FAILED"
csnrStartEpoch row `shouldBe` Just "1445616705"
csnrStartTime row `shouldBe` Just "18:11:45"
csnrEndEpoch row `shouldBe` Just "1445616978"
csnrEndTime row `shouldBe` Just "18:16:18"
csnrDuration row `shouldBe` Just "4.6m"
csnrIndices row `shouldBe` Just "1"
csnrSuccessfulShards row `shouldBe` Just "4"
csnrFailedShards row `shouldBe` Just "1"
csnrTotalShards row `shouldBe` Just "5"
csnrReason row `shouldBe` Nothing
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses a minimal row containing only id" $
case parseEither parseJSON =<< eitherDecode minimalRow :: Either String [CatSnapshotsRow] of
Right (row : _) -> do
csnrId row `shouldBe` Just "snap1"
csnrStatus row `shouldBe` Nothing
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in csnrOther" $
case parseEither parseJSON =<< eitherDecode extraColumnRow :: Either String [CatSnapshotsRow] of
Right (row : _) ->
case csnrOther row of
Object o -> do
KM.keys o `shouldContain` ["id"]
KM.keys o `shouldContain` ["future.column"]
_ -> expectationFailure "expected csnrOther to be an Object"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses an empty snapshots list" $
case parseEither parseJSON =<< eitherDecode "[]" :: Either String [CatSnapshotsRow] of
Right rows -> rows `shouldBe` []
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catSnapshots integration (live ES) --
-- ------------------------------------------------------------------ --
describe "catSnapshots integration" $
it "returns a (possibly empty) list without throwing" $
withTestEnv $ do
-- The cat snapshots endpoint requires a registered snapshot
-- repository; @GET /_cat/snapshots@ with no repo returns HTTP
-- 400 "repository is missing" on every supported backend. The
-- docker-compose test cluster doesn't configure @path.repo@,
-- so an fs repository cannot be registered from the test
-- either. Skip the live check when no repo is available,
-- rather than failing on infrastructure the test cannot
-- provision.
repoResult <-
tryPerformBHRequest $
catSnapshotsWith
(Just (SnapshotRepoName "_all"))
defaultCatSnapshotsOptions
case repoResult of
Right rows -> liftIO $ rows `shouldSatisfy` (\_ -> True)
Left e
| "repository" `T.isInfixOf` errorMessage e
|| "no such" `T.isInfixOf` errorMessage e
|| "missing" `T.isInfixOf` errorMessage e ->
liftIO $ pendingWith "no snapshot repository registered on this cluster"
Left e -> liftIO $ expectationFailure ("catSnapshots failed unexpectedly: " <> show e)
-- ------------------------------------------------------------------ --
-- catNodesOptionsParams: pure URI rendering (no ES required) --
-- ------------------------------------------------------------------ --
describe "catNodesOptionsParams URI rendering" $ do
it "defaultCatNodesOptions emits only format=json" $
catNodesOptionsParams defaultCatNodesOptions
`shouldBe` [("format", Just "json")]
it "renders bytes via catBytesSizeText" $ do
let opts = defaultCatNodesOptions {cnoBytes = Just CatBytesKb}
catNodesOptionsParams opts
`shouldContain` [("bytes", Just "kb")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatNodesOptions
{ cnoColumns = Just (CatNodesColName :| [CatNodesColHeapPercent, CatNodesColCpu])
}
catNodesOptionsParams opts
`shouldContain` [("h", Just "name,heap.percent,cpu")]
it "renders unknown columns verbatim" $ do
let opts =
defaultCatNodesOptions
{ cnoColumns = Just (CatNodesColOther "uptime" :| [])
}
catNodesOptionsParams opts
`shouldContain` [("h", Just "uptime")]
it "renders booleans as true/false" $ do
let opts =
defaultCatNodesOptions
{ cnoHelp = Just True,
cnoLocal = Just False,
cnoVerbose = Just True
}
let ps = catNodesOptionsParams opts
ps `shouldContain` [("help", Just "true")]
ps `shouldContain` [("local", Just "false")]
ps `shouldContain` [("v", Just "true")]
it "renders master_timeout via timeUnitsSuffix" $ do
let opts = defaultCatNodesOptions {cnoMasterTimeout = Just (TimeUnitSeconds, 30)}
catNodesOptionsParams opts
`shouldContain` [("master_timeout", Just "30s")]
it "renders time via timeUnitsSuffix" $ do
let opts = defaultCatNodesOptions {cnoTime = Just (TimeUnitMinutes, 5)}
catNodesOptionsParams opts
`shouldContain` [("time", Just "5m")]
it "renders sort specs as comma-joined column[:direction] terms" $ do
let opts =
defaultCatNodesOptions
{ cnoSort =
Just
( CatNodesSortSpec CatNodesColName (Just CatSortAsc)
:| [CatNodesSortSpec CatNodesColCpu Nothing]
)
}
catNodesOptionsParams opts
`shouldContain` [("s", Just "name:asc,cpu")]
describe "catSegmentsColumnText rendering" $
it "covers every documented column" $
map
catSegmentsColumnText
[ CatSegmentsColIndex,
CatSegmentsColShard,
CatSegmentsColPrirep,
CatSegmentsColIp,
CatSegmentsColSegment,
CatSegmentsColId,
CatSegmentsColGeneration,
CatSegmentsColDocsCount,
CatSegmentsColDocsDeleted,
CatSegmentsColSize,
CatSegmentsColSizeMemory,
CatSegmentsColCommitted,
CatSegmentsColSearchable,
CatSegmentsColVersion,
CatSegmentsColCompound
]
`shouldBe` [ "index",
"shard",
"prirep",
"ip",
"segment",
"id",
"generation",
"docs.count",
"docs.deleted",
"size",
"size.memory",
"committed",
"searchable",
"version",
"compound"
]
-- ------------------------------------------------------------------ --
-- CatSegmentsRow JSON decoding --
-- ------------------------------------------------------------------ --
describe "CatSegmentsRow FromJSON" $ do
let fullRow =
"[{\"index\":\"test\",\"shard\":\"0\",\"prirep\":\"p\",\"ip\":\"127.0.0.1\",\"segment\":\"_0\",\"id\":\"abc123\",\"generation\":\"0\",\"docs.count\":\"42\",\"docs.deleted\":\"5\",\"size\":\"5kb\",\"size.memory\":\"208\",\"committed\":\"true\",\"searchable\":\"true\",\"version\":\"9.0.0\",\"compound\":\"true\"}]"
minimalRow = "[{\"index\":\"test\",\"shard\":\"0\"}]"
extraColumnRow = "[{\"index\":\"x\",\"custom.col\":\"xyz\"}]"
it "parses a fully-populated default row" $
case parseEither parseJSON =<< eitherDecode fullRow :: Either String [CatSegmentsRow] of
Right (row : _) -> do
cserIndex row `shouldBe` Just "test"
cserShard row `shouldBe` Just 0
cserPrirep row `shouldBe` Just "p"
cserIp row `shouldBe` Just "127.0.0.1"
cserSegment row `shouldBe` Just "_0"
cserId row `shouldBe` Just "abc123"
cserGeneration row `shouldBe` Just 0
cserDocsCount row `shouldBe` Just 42
cserDocsDeleted row `shouldBe` Just 5
cserCommitted row `shouldBe` Just True
cserSearchable row `shouldBe` Just True
cserVersion row `shouldBe` Just "9.0.0"
cserCompound row `shouldBe` Just True
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses a minimal row containing only index and shard" $
case parseEither parseJSON =<< eitherDecode minimalRow :: Either String [CatSegmentsRow] of
Right (row : _) -> do
cserIndex row `shouldBe` Just "test"
cserShard row `shouldBe` Just 0
cserCompound row `shouldBe` Nothing
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in cserOther" $
case parseEither parseJSON =<< eitherDecode extraColumnRow :: Either String [CatSegmentsRow] of
Right (row : _) ->
case cserOther row of
Object o -> do
KM.keys o `shouldContain` ["index"]
KM.keys o `shouldContain` ["custom.col"]
_ -> expectationFailure "expected cserOther to be an Object"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates native JSON numbers for shard and generation" $
case parseEither parseJSON =<< eitherDecode "[{\"shard\":3,\"generation\":7,\"docs.count\":100}]" :: Either String [CatSegmentsRow] of
Right (row : _) -> do
cserShard row `shouldBe` Just 3
cserGeneration row `shouldBe` Just 7
cserDocsCount row `shouldBe` Just 100
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates native JSON booleans for committed/searchable/compound" $
case parseEither parseJSON =<< eitherDecode "[{\"committed\":false,\"searchable\":true,\"compound\":false}]" :: Either String [CatSegmentsRow] of
Right (row : _) -> do
cserCommitted row `shouldBe` Just False
cserSearchable row `shouldBe` Just True
cserCompound row `shouldBe` Just False
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses an empty segments list" $
case parseEither parseJSON =<< eitherDecode "[]" :: Either String [CatSegmentsRow] of
Right rows -> rows `shouldBe` []
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catSegments integration (live ES) --
-- ------------------------------------------------------------------ --
describe "catSegments integration" $
it "returns a (possibly empty) list without throwing" $
withTestEnv $ do
_ <- Client.catSegments
pure ()
-- ------------------------------------------------------------------ --
-- catRecoveryOptionsParams: pure URI rendering (no ES required) --
-- ------------------------------------------------------------------ --
describe "catRecoveryOptionsParams URI rendering" $ do
it "defaultCatRecoveryOptions emits format=json and bytes=b" $
catRecoveryOptionsParams defaultCatRecoveryOptions
`shouldBe` [("format", Just "json"), ("bytes", Just "b")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatRecoveryOptions
{ cryoColumns = Just (CatRecoveryColIndex :| [CatRecoveryColShard, CatRecoveryColStage])
}
catRecoveryOptionsParams opts
`shouldContain` [("h", Just "index,shard,stage")]
it "renders bytes via catBytesSizeText" $ do
let opts = defaultCatRecoveryOptions {cryoBytes = Just CatBytesMb}
catRecoveryOptionsParams opts
`shouldContain` [("bytes", Just "mb")]
it "renders active_only as true" $ do
let opts = defaultCatRecoveryOptions {cryoActiveOnly = Just True}
catRecoveryOptionsParams opts
`shouldContain` [("active_only", Just "true")]
it "renders detailed as true" $ do
let opts = defaultCatRecoveryOptions {cryoDetailed = Just True}
catRecoveryOptionsParams opts
`shouldContain` [("detailed", Just "true")]
it "renders booleans as true/false" $ do
let opts =
defaultCatRecoveryOptions
{ cryoHelp = Just False,
cryoLocal = Just True,
cryoVerbose = Just True
}
let ps = catRecoveryOptionsParams opts
ps `shouldContain` [("help", Just "false")]
ps `shouldContain` [("local", Just "true")]
ps `shouldContain` [("v", Just "true")]
it "renders master_timeout via timeUnitsSuffix" $ do
let opts = defaultCatRecoveryOptions {cryoMasterTimeout = Just (TimeUnitSeconds, 30)}
catRecoveryOptionsParams opts
`shouldContain` [("master_timeout", Just "30s")]
it "renders sort specs as comma-joined column[:direction] terms" $ do
let opts =
defaultCatRecoveryOptions
{ cryoSort =
Just
( CatRecoverySortSpec CatRecoveryColIndex (Just CatSortAsc)
:| [CatRecoverySortSpec CatRecoveryColStage Nothing]
)
}
catRecoveryOptionsParams opts
`shouldContain` [("s", Just "index:asc,stage")]
-- ------------------------------------------------------------------ --
-- Renderer sanity for the leaf sum types --
-- ------------------------------------------------------------------ --
describe "catNodesColumnText rendering" $
it "covers every documented column" $
map
catNodesColumnText
[ CatNodesColIp,
CatNodesColPort,
CatNodesColHost,
CatNodesColName,
CatNodesColId,
CatNodesColPid,
CatNodesColMaster,
CatNodesColNodeRole,
CatNodesColHeapPercent,
CatNodesColRamPercent,
CatNodesColCpu,
CatNodesColLoad1m,
CatNodesColLoad5m,
CatNodesColLoad15m,
CatNodesColHeapCurrent,
CatNodesColHeapMax,
CatNodesColRamCurrent,
CatNodesColRamMax,
CatNodesColDiskUsedPercent,
CatNodesColDiskUsed,
CatNodesColDiskAvail,
CatNodesColDiskTotal,
CatNodesColFileDescPercent,
CatNodesColFileDescCurrent,
CatNodesColFileDescMax
]
`shouldBe` [ "ip",
"port",
"host",
"name",
"id",
"pid",
"master",
"node.role",
"heap.percent",
"ram.percent",
"cpu",
"load_1m",
"load_5m",
"load_15m",
"heap.current",
"heap.max",
"ram.current",
"ram.max",
"disk.used_percent",
"disk.used",
"disk.avail",
"disk.total",
"file_desc.percent",
"file_desc.current",
"file_desc.max"
]
describe "catNodesMasterFromText / ToText round-trip" $ do
it "decodes \"*\" as the elected master" $
catNodesMasterFromText "*" `shouldBe` Just CatNodesMasterTrue
it "decodes \"-\" as a non-master node" $
catNodesMasterFromText "-" `shouldBe` Just CatNodesMasterFalse
it "rejects unknown sentinels" $
catNodesMasterFromText "maybe" `shouldBe` (Nothing :: Maybe CatNodesMaster)
it "round-trips every constructor through ToText" $ do
catNodesMasterToText CatNodesMasterTrue `shouldBe` "*"
catNodesMasterToText CatNodesMasterFalse `shouldBe` "-"
-- ------------------------------------------------------------------ --
-- CatNodesRow JSON decoding --
-- ------------------------------------------------------------------ --
describe "CatNodesRow FromJSON" $ do
let fullRow =
"{\
\ \"ip\":\"127.0.0.1\",\
\ \"port\":\"9300\",\
\ \"host\":\"127.0.0.1\",\
\ \"name\":\"node-1\",\
\ \"id\":\"CSUXak2TQcK3CxFvPJyFtQ\",\
\ \"pid\":\"42\",\
\ \"master\":\"*\",\
\ \"node.role\":\"dimr\",\
\ \"heap.percent\":\"45\",\
\ \"ram.percent\":\"60\",\
\ \"cpu\":\"12\",\
\ \"load_1m\":\"0.07\",\
\ \"load_5m\":\"0.10\",\
\ \"load_15m\":\"0.15\",\
\ \"heap.current\":\"1.4gb\",\
\ \"heap.max\":\"4gb\",\
\ \"ram.current\":\"6.5gb\",\
\ \"ram.max\":\"16gb\",\
\ \"disk.used_percent\":\"46\",\
\ \"disk.used\":\"47.3gb\",\
\ \"disk.avail\":\"53.4gb\",\
\ \"disk.total\":\"100.7gb\",\
\ \"file_desc.percent\":\"3\",\
\ \"file_desc.current\":\"312\",\
\ \"file_desc.max\":\"65536\"\
\}"
minimalRow = "{\"name\":\"bloodhound-tests-cat-node\"}"
subsetRow =
"{\
\ \"name\":\"bloodhound-tests-cat-node\",\
\ \"heap.percent\":\"50\",\
\ \"cpu\":\"99\"\
\}"
nonMasterRow =
"{\
\ \"name\":\"bloodhound-tests-cat-node\",\
\ \"master\":\"-\"\
\}"
fractionalRow =
"{\
\ \"name\":\"bloodhound-tests-cat-node\",\
\ \"heap.current\":\"5.2gb\",\
\ \"heap.max\":\"5.2gb\"\
\}"
bareRow =
"{\
\ \"name\":\"bloodhound-tests-cat-node\",\
\ \"heap.current\":\"208\",\
\ \"heap.max\":\"208\"\
\}"
extraColumnRow =
"{\
\ \"name\":\"bloodhound-tests-cat-node\",\
\ \"uptime\":\"5m\"\
\}"
badUnitRow =
"{\
\ \"name\":\"bloodhound-tests-cat-node\",\
\ \"heap.current\":\"5qb\"\
\}"
openSearchMasterRow =
"{\
\ \"name\":\"bloodhound-tests-cat-node\",\
\ \"cluster_manager\":\"*\"\
\}"
bothMasterColumnsRow =
"{\
\ \"name\":\"bloodhound-tests-cat-node\",\
\ \"master\":\"-\",\
\ \"cluster_manager\":\"*\"\
\}"
invalidMasterRow =
"{\
\ \"name\":\"bloodhound-tests-cat-node\",\
\ \"master\":\"maybe\"\
\}"
it "parses a fully-populated default row" $
case parseEither parseJSON =<< eitherDecode fullRow :: Either String CatNodesRow of
Right row -> do
cnrIp row `shouldBe` Just "127.0.0.1"
cnrPort row `shouldBe` Just 9300
cnrHost row `shouldBe` Just "127.0.0.1"
cnrName row `shouldBe` "node-1"
cnrId row `shouldBe` Just (FullNodeId "CSUXak2TQcK3CxFvPJyFtQ")
cnrPid row `shouldBe` Just 42
cnrMaster row `shouldBe` Just CatNodesMasterTrue
cnrNodeRole row `shouldBe` Just "dimr"
cnrHeapPercent row `shouldBe` Just 45
cnrRamPercent row `shouldBe` Just 60
cnrCpu row `shouldBe` Just 12
cnrLoad1m row `shouldBe` Just 0.07
cnrLoad5m row `shouldBe` Just 0.10
cnrLoad15m row `shouldBe` Just 0.15
cnrHeapCurrent row `shouldBe` Just (Bytes 1, CatBytesGb)
cnrHeapMax row `shouldBe` Just (Bytes 4, CatBytesGb)
cnrRamCurrent row `shouldBe` Just (Bytes 6, CatBytesGb)
cnrRamMax row `shouldBe` Just (Bytes 16, CatBytesGb)
cnrDiskUsedPercent row `shouldBe` Just 46
cnrDiskUsed row `shouldBe` Just (Bytes 47, CatBytesGb)
cnrDiskAvail row `shouldBe` Just (Bytes 53, CatBytesGb)
cnrDiskTotal row `shouldBe` Just (Bytes 101, CatBytesGb)
cnrFileDescPercent row `shouldBe` Just 3
cnrFileDescCurrent row `shouldBe` Just 312
cnrFileDescMax row `shouldBe` Just 65536
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses a minimal row containing only `name`" $
case parseEither parseJSON =<< eitherDecode minimalRow :: Either String CatNodesRow of
Right row -> do
cnrName row `shouldBe` "bloodhound-tests-cat-node"
cnrIp row `shouldBe` Nothing
cnrMaster row `shouldBe` Nothing
cnrHeapPercent row `shouldBe` Nothing
cnrCpu row `shouldBe` Nothing
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses a column-subset row (as produced by h=name,heap.percent,cpu)" $
case parseEither parseJSON =<< eitherDecode subsetRow :: Either String CatNodesRow of
Right row -> do
cnrName row `shouldBe` "bloodhound-tests-cat-node"
cnrHeapPercent row `shouldBe` Just 50
cnrCpu row `shouldBe` Just 99
-- Columns we did not request should be absent:
cnrRamPercent row `shouldBe` Nothing
cnrLoad1m row `shouldBe` Nothing
Left e -> expectationFailure ("expected parse, got " <> e)
it "decodes the \"-\" sentinel for master as CatNodesMasterFalse" $
case parseEither parseJSON =<< eitherDecode nonMasterRow :: Either String CatNodesRow of
Right row -> cnrMaster row `shouldBe` Just CatNodesMasterFalse
Left e -> expectationFailure ("expected parse, got " <> e)
it "falls back to the cluster_manager column when master is absent (OpenSearch)" $
case parseEither parseJSON =<< eitherDecode openSearchMasterRow :: Either String CatNodesRow of
Right row -> cnrMaster row `shouldBe` Just CatNodesMasterTrue
Left e -> expectationFailure ("expected parse, got " <> e)
-- OS3 emits both @master@ (when explicitly requested via @h=master@)
-- and @cluster_manager@ in default output. Locks in that the ES
-- column name wins when both are present, so the parser is
-- deterministic regardless of which alias the server happens to
-- emit.
it "prefers master over cluster_manager when both are present" $
case parseEither parseJSON =<< eitherDecode bothMasterColumnsRow :: Either String CatNodesRow of
Right row -> cnrMaster row `shouldBe` Just CatNodesMasterFalse
Left e -> expectationFailure ("expected parse, got " <> e)
it "rejects an unrecognised master sentinel" $
case parseEither parseJSON =<< eitherDecode invalidMasterRow :: Either String CatNodesRow of
Right row -> expectationFailure ("expected parse failure, got " <> show row)
Left _ -> return ()
it "rounds fractional heap magnitudes to the nearest whole byte" $
case parseEither parseJSON =<< eitherDecode fractionalRow :: Either String CatNodesRow of
Right row -> do
cnrHeapCurrent row `shouldBe` Just (Bytes 5, CatBytesGb)
cnrHeapMax row `shouldBe` Just (Bytes 5, CatBytesGb)
Left e -> expectationFailure ("expected parse, got " <> e)
it "treats a bare number as CatBytesBytes" $
case parseEither parseJSON =<< eitherDecode bareRow :: Either String CatNodesRow of
Right row -> do
cnrHeapCurrent row `shouldBe` Just (Bytes 208, CatBytesBytes)
cnrHeapMax row `shouldBe` Just (Bytes 208, CatBytesBytes)
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in cnrOther" $
case parseEither parseJSON =<< eitherDecode extraColumnRow :: Either String CatNodesRow of
Right row ->
case cnrOther row of
Object o -> KM.keys o `shouldContain` ["name", "uptime"]
_ -> expectationFailure "expected cnrOther to be an Object"
Left e -> expectationFailure ("expected parse, got " <> e)
it "rejects an unrecognised byte-size suffix" $
case parseEither parseJSON =<< eitherDecode badUnitRow :: Either String CatNodesRow of
Right row -> expectationFailure ("expected parse failure, got " <> show row)
Left _ -> return ()
-- Defensive coverage: if OpenSearch emits a native JSON number for
-- an integer column (instead of the usual string), the parser
-- should still succeed by stringifying the integer-valued
-- Scientific without a trailing ".0". See 'parseAsString'.
it "tolerates a native JSON number for an integer column (cpu)" $
case parseEither parseJSON =<< eitherDecode "{\"name\":\"x\",\"cpu\":99}" :: Either String CatNodesRow of
Right row -> cnrCpu row `shouldBe` Just 99
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates a native JSON number for heap.percent" $
case parseEither parseJSON =<< eitherDecode "{\"name\":\"x\",\"heap.percent\":50}" :: Either String CatNodesRow of
Right row -> cnrHeapPercent row `shouldBe` Just 50
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates a native JSON number for load_1m (fractional)" $
case parseEither parseJSON =<< eitherDecode "{\"name\":\"x\",\"load_1m\":0.07}" :: Either String CatNodesRow of
Right row -> cnrLoad1m row `shouldBe` Just 0.07
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates a native JSON number for heap.current (treated as bare bytes)" $
case parseEither parseJSON =<< eitherDecode "{\"name\":\"x\",\"heap.current\":208}" :: Either String CatNodesRow of
Right row -> cnrHeapCurrent row `shouldBe` Just (Bytes 208, CatBytesBytes)
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catNodesWith integration (live ES) --
-- ------------------------------------------------------------------ --
describe "catNodesWith integration" $ do
it "returns at least one node row" $
withTestEnv $ do
rows <- Client.catNodes
liftIO $
rows `shouldSatisfy` (not . null)
-- @_cat\/nodes@ does not accept a path-segment filter on either ES
-- or OS (unlike @_cat\/allocation@). Locks in that we see exactly
-- one elected master in the default output, so the @master@ column
-- round-trips the @"*"@ sentinel through 'CatNodesMaster' and the
-- parser doesn't accidentally mark every node as master.
it "exposes exactly one elected master via the master sentinel" $
withTestEnv $ do
rows <- Client.catNodes
liftIO $
length (filter ((== Just CatNodesMasterTrue) . cnrMaster) rows) `shouldBe` 1
it "honours the h= parameter and leaves unrequested columns absent" $
withTestEnv $ do
allRows <- Client.catNodes
case allRows of
[] -> liftIO $ expectationFailure "expected at least one node row"
(firstRow : _) -> do
let name = cnrName firstRow
opts =
defaultCatNodesOptions
{ cnoColumns = Just (CatNodesColName :| [CatNodesColCpu])
}
filtered <- Client.catNodesWith opts
liftIO $
case filter ((== name) . cnrName) filtered of
(row : _) -> do
-- Requested columns come back populated:
cnrName row `shouldBe` name
cnrCpu row `shouldSatisfy` isJust
-- Columns we did not request should be absent:
cnrIp row `shouldBe` Nothing
cnrHost row `shouldBe` Nothing
cnrHeapPercent row `shouldBe` Nothing
[] ->
expectationFailure "expected at least one row for the freshly-listed node"
describe "catRecoveryColumnText rendering" $
it "covers every documented column" $
map
catRecoveryColumnText
[ CatRecoveryColIndex,
CatRecoveryColShard,
CatRecoveryColStartTime,
CatRecoveryColStartTimeMillis,
CatRecoveryColStopTime,
CatRecoveryColStopTimeMillis,
CatRecoveryColTime,
CatRecoveryColType,
CatRecoveryColStage,
CatRecoveryColSourceHost,
CatRecoveryColSourceNode,
CatRecoveryColTargetHost,
CatRecoveryColTargetNode,
CatRecoveryColRepository,
CatRecoveryColSnapshot,
CatRecoveryColFiles,
CatRecoveryColFilesRecovered,
CatRecoveryColFilesPercent,
CatRecoveryColFilesTotal,
CatRecoveryColBytes,
CatRecoveryColBytesRecovered,
CatRecoveryColBytesPercent,
CatRecoveryColBytesTotal,
CatRecoveryColTranslogOps,
CatRecoveryColTranslogOpsRecovered,
CatRecoveryColTranslogOpsPercent
]
`shouldBe` [ "index",
"shard",
"start_time",
"start_time_millis",
"stop_time",
"stop_time_millis",
"time",
"type",
"stage",
"source_host",
"source_node",
"target_host",
"target_node",
"repository",
"snapshot",
"files",
"files_recovered",
"files_percent",
"files_total",
"bytes",
"bytes_recovered",
"bytes_percent",
"bytes_total",
"translog_ops",
"translog_ops_recovered",
"translog_ops_percent"
]
-- ------------------------------------------------------------------ --
-- CatRecoveryRow JSON decoding --
-- ------------------------------------------------------------------ --
describe "CatRecoveryRow FromJSON" $ do
let fullRow =
"[{\"index\":\"test\",\"shard\":\"0\",\"start_time\":\"2024-01-01T00:00:00.000Z\",\"start_time_millis\":\"1704067200000\",\"stop_time\":\"2024-01-01T00:00:05.000Z\",\"stop_time_millis\":\"1704067205000\",\"time\":\"5s\",\"type\":\"store\",\"stage\":\"DONE\",\"source_host\":\"10.0.0.1\",\"source_node\":\"node-1\",\"target_host\":\"10.0.0.2\",\"target_node\":\"node-2\",\"repository\":\"repo1\",\"snapshot\":\"snap1\",\"files\":\"10\",\"files_recovered\":\"10\",\"files_percent\":\"100.0%\",\"files_total\":\"10\",\"bytes\":\"2048\",\"bytes_recovered\":\"2048\",\"bytes_percent\":\"100.0%\",\"bytes_total\":\"2048\",\"translog_ops\":\"5\",\"translog_ops_recovered\":\"5\",\"translog_ops_percent\":\"100.0%\"}]"
minimalRow = "[{\"index\":\"test\",\"shard\":\"0\"}]"
extraColumnRow = "[{\"index\":\"x\",\"custom.col\":\"xyz\"}]"
it "parses a fully-populated default row" $
case parseEither parseJSON =<< eitherDecode fullRow :: Either String [CatRecoveryRow] of
Right (row : _) -> do
cryrIndex row `shouldBe` Just "test"
cryrShard row `shouldBe` Just 0
cryrStartTime row `shouldBe` Just "2024-01-01T00:00:00.000Z"
cryrStartTimeMillis row `shouldBe` Just 1704067200000
cryrStopTime row `shouldBe` Just "2024-01-01T00:00:05.000Z"
cryrStopTimeMillis row `shouldBe` Just 1704067205000
cryrTime row `shouldBe` Just "5s"
cryrType row `shouldBe` Just "store"
cryrStage row `shouldBe` Just "DONE"
cryrSourceHost row `shouldBe` Just "10.0.0.1"
cryrSourceNode row `shouldBe` Just "node-1"
cryrTargetHost row `shouldBe` Just "10.0.0.2"
cryrTargetNode row `shouldBe` Just "node-2"
cryrRepository row `shouldBe` Just "repo1"
cryrSnapshot row `shouldBe` Just "snap1"
cryrFiles row `shouldBe` Just 10
cryrFilesRecovered row `shouldBe` Just 10
cryrFilesPercent row `shouldBe` Just "100.0%"
cryrFilesTotal row `shouldBe` Just 10
cryrBytes row `shouldBe` Just 2048
cryrBytesRecovered row `shouldBe` Just 2048
cryrBytesPercent row `shouldBe` Just "100.0%"
cryrBytesTotal row `shouldBe` Just 2048
cryrTranslogOps row `shouldBe` Just 5
cryrTranslogOpsRecovered row `shouldBe` Just 5
cryrTranslogOpsPercent row `shouldBe` Just "100.0%"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses a minimal row containing only index and shard" $
case parseEither parseJSON =<< eitherDecode minimalRow :: Either String [CatRecoveryRow] of
Right (row : _) -> do
cryrIndex row `shouldBe` Just "test"
cryrShard row `shouldBe` Just 0
cryrStage row `shouldBe` Nothing
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in cryrOther" $
case parseEither parseJSON =<< eitherDecode extraColumnRow :: Either String [CatRecoveryRow] of
Right (row : _) ->
case cryrOther row of
Object o -> do
KM.keys o `shouldContain` ["index"]
KM.keys o `shouldContain` ["custom.col"]
_ -> expectationFailure "expected cryrOther to be an Object"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates native JSON numbers for shard and bytes" $
case parseEither parseJSON =<< eitherDecode "[{\"shard\":3,\"bytes\":4096,\"start_time_millis\":1704067200000}]" :: Either String [CatRecoveryRow] of
Right (row : _) -> do
cryrShard row `shouldBe` Just 3
cryrBytes row `shouldBe` Just 4096
cryrStartTimeMillis row `shouldBe` Just 1704067200000
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "parses an empty recovery list" $
case parseEither parseJSON =<< eitherDecode "[]" :: Either String [CatRecoveryRow] of
Right rows -> rows `shouldBe` []
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catRecovery integration (live ES) --
-- ------------------------------------------------------------------ --
describe "catRecovery integration" $
it "returns a (possibly empty) list without throwing" $
withTestEnv $ do
_ <- Client.catRecovery
pure ()
-- ------------------------------------------------------------------ --
-- catComponentTemplatesOptionsParams: pure URI rendering --
-- ------------------------------------------------------------------ --
describe "catComponentTemplatesOptionsParams URI rendering" $ do
it "default emits only format=json" $
catComponentTemplatesOptionsParams defaultCatComponentTemplatesOptions
`shouldBe` [("format", Just "json")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatComponentTemplatesOptions
{ cctoColumns = Just (CatComponentTemplatesColName :| [CatComponentTemplatesColIncludedIn])
}
catComponentTemplatesOptionsParams opts
`shouldContain` [("h", Just "name,included_in")]
it "renders sort (s) as a comma-joined list" $ do
let opts = defaultCatComponentTemplatesOptions {cctoSort = Just ("name" :| ["version:desc"])}
catComponentTemplatesOptionsParams opts
`shouldContain` [("s", Just "name,version:desc")]
it "renders booleans and master_timeout" $ do
let opts =
defaultCatComponentTemplatesOptions
{ cctoHelp = Just True,
cctoLocal = Just False,
cctoVerbose = Just True,
cctoMasterTimeout = Just (TimeUnitSeconds, 30)
}
let ps = catComponentTemplatesOptionsParams opts
ps `shouldContain` [("help", Just "true")]
ps `shouldContain` [("local", Just "false")]
ps `shouldContain` [("v", Just "true")]
ps `shouldContain` [("master_timeout", Just "30s")]
describe "catComponentTemplatesColumnText rendering" $
it "covers documented columns" $
map
catComponentTemplatesColumnText
[CatComponentTemplatesColName, CatComponentTemplatesColAliasCount, CatComponentTemplatesColIncludedIn]
`shouldBe` ["name", "alias_count", "included_in"]
describe "CatComponentTemplatesRow FromJSON" $ do
let fullRow =
"[{\"name\":\"my-template-1\",\"version\":\"null\",\"alias_count\":\"0\",\
\\"mapping_count\":\"0\",\"settings_count\":\"1\",\"metadata_count\":\"0\",\
\\"included_in\":\"[my-index-template]\"}]"
it "parses a fully-populated row" $
case parseEither parseJSON =<< eitherDecode fullRow :: Either String [CatComponentTemplatesRow] of
Right (row : _) -> do
cctrName row `shouldBe` Just "my-template-1"
cctrVersion row `shouldBe` Just "null"
cctrAliasCount row `shouldBe` Just 0
cctrSettingsCount row `shouldBe` Just 1
cctrIncludedIn row `shouldBe` Just "[my-index-template]"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "maps a native JSON null version to Nothing" $
case parseEither parseJSON =<< eitherDecode "[{\"name\":\"t2\",\"version\":null}]" :: Either String [CatComponentTemplatesRow] of
Right (row : _) -> cctrVersion row `shouldBe` Nothing
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in cctrOther" $
case parseEither parseJSON =<< eitherDecode "[{\"name\":\"t3\",\"future.col\":\"x\"}]" :: Either String [CatComponentTemplatesRow] of
Right (row : _) ->
case cctrOther row of
Object o -> do
KM.keys o `shouldSatisfy` ("name" `elem`)
KM.keys o `shouldSatisfy` ("future.col" `elem`)
_ -> expectationFailure "expected cctrOther to be an Object"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates native JSON numbers for counts" $
case parseEither parseJSON =<< eitherDecode "[{\"alias_count\":5,\"mapping_count\":2}]" :: Either String [CatComponentTemplatesRow] of
Right (row : _) -> do
cctrAliasCount row `shouldBe` Just 5
cctrMappingCount row `shouldBe` Just 2
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
describe "catComponentTemplates integration" $
backendSpecific [ElasticSearch8, ElasticSearch9] $
it "returns a (possibly empty) list without throwing" $
withTestEnv $ do
_ <- Client.catComponentTemplates
pure ()
-- ------------------------------------------------------------------ --
-- catCircuitBreakersOptionsParams: pure URI rendering --
-- ------------------------------------------------------------------ --
describe "catCircuitBreakersOptionsParams URI rendering" $ do
it "default emits only format=json" $
catCircuitBreakersOptionsParams defaultCatCircuitBreakersOptions
`shouldBe` [("format", Just "json")]
it "renders columns (h) as a comma-joined list" $ do
let opts =
defaultCatCircuitBreakersOptions
{ ccboColumns = Just (CatCircuitBreakersColBreaker :| [CatCircuitBreakersColTripped])
}
catCircuitBreakersOptionsParams opts
`shouldContain` [("h", Just "breaker,tripped")]
it "renders booleans and master_timeout" $ do
let opts =
defaultCatCircuitBreakersOptions
{ ccboLocal = Just True,
ccboMasterTimeout = Just (TimeUnitMinutes, 1)
}
let ps = catCircuitBreakersOptionsParams opts
ps `shouldContain` [("local", Just "true")]
ps `shouldContain` [("master_timeout", Just "1m")]
describe "CatCircuitBreakersRow FromJSON" $ do
let fullRow =
"[{\"node_id\":\"ozKxpP9oS3SL0Sp-Mfxc6w\",\"node_name\":\"node-1\",\
\\"breaker\":\"request\",\"limit\":\"614.3mb\",\"limit_bytes\":\"644245094\",\
\\"estimated\":\"0b\",\"estimated_bytes\":\"0\",\"tripped\":\"0\",\"overhead\":\"1.03\"}]"
it "parses a fully-populated row" $
case parseEither parseJSON =<< eitherDecode fullRow :: Either String [CatCircuitBreakersRow] of
Right (row : _) -> do
ccbrBreaker row `shouldBe` Just "request"
ccbrLimit row `shouldBe` Just "614.3mb"
ccbrLimitBytes row `shouldBe` Just 644245094
ccbrEstimatedBytes row `shouldBe` Just 0
ccbrTripped row `shouldBe` Just 0
ccbrOverhead row `shouldBe` Just "1.03"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates native JSON numbers for bytes/tripped" $
case parseEither parseJSON =<< eitherDecode "[{\"tripped\":3,\"limit_bytes\":1024}]" :: Either String [CatCircuitBreakersRow] of
Right (row : _) -> do
ccbrTripped row `shouldBe` Just 3
ccbrLimitBytes row `shouldBe` Just 1024
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "captures unknown columns verbatim in ccbrOther" $
case parseEither parseJSON =<< eitherDecode "[{\"breaker\":\"x\",\"future.col\":\"y\"}]" :: Either String [CatCircuitBreakersRow] of
Right (row : _) ->
case ccbrOther row of
Object o -> do
KM.keys o `shouldSatisfy` ("breaker" `elem`)
KM.keys o `shouldSatisfy` ("future.col" `elem`)
_ -> expectationFailure "expected ccbrOther to be an Object"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
describe "catCircuitBreakers integration" $
backendSpecific [ElasticSearch9] $
it "returns a non-empty list on any cluster" $
withTestEnv $ do
rows <- Client.catCircuitBreakers
liftIO $ rows `shouldSatisfy` (not . null)
-- ------------------------------------------------------------------ --
-- catMlJobsOptionsParams / CatMlJobsRow --
-- ------------------------------------------------------------------ --
describe "catMlJobsOptionsParams URI rendering" $ do
it "default emits only format=json" $
catMlJobsOptionsParams defaultCatMlJobsOptions
`shouldBe` [("format", Just "json")]
it "renders allow_no_match and columns" $ do
let opts =
defaultCatMlJobsOptions
{ cmjoAllowNoMatch = Just True,
cmjoColumns = Just (CatMlJobsColId :| [CatMlJobsColState])
}
let ps = catMlJobsOptionsParams opts
ps `shouldContain` [("allow_no_match", Just "true")]
ps `shouldContain` [("h", Just "id,state")]
describe "CatMlJobsRow FromJSON" $ do
it "parses a row with dotted keys" $
case parseEither parseJSON =<< eitherDecode "[{\"id\":\"high_sum_total_sales\",\"state\":\"closed\",\"data.processed_records\":\"14022\",\"model.bytes\":\"1600000\"}]" :: Either String [CatMlJobsRow] of
Right (row : _) -> do
cmjrId row `shouldBe` Just "high_sum_total_sales"
cmjrState row `shouldBe` Just "closed"
cmjrDataProcessedRecords row `shouldBe` Just 14022
cmjrModelBytes row `shouldBe` Just 1600000
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "tolerates native JSON numbers and captures extras in cmjrOther" $
case parseEither parseJSON =<< eitherDecode "[{\"id\":\"j\",\"buckets.count\":10,\"model.memory_status\":\"ok\",\"future.col\":\"z\"}]" :: Either String [CatMlJobsRow] of
Right (row : _) -> do
cmjrBucketsCount row `shouldBe` Just 10
cmjrModelMemoryStatus row `shouldBe` Just "ok"
case cmjrOther row of
Object o -> KM.keys o `shouldContain` ["future.col"]
_ -> expectationFailure "expected cmjrOther to be an Object"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catMlDataFrameAnalyticsOptionsParams / CatMlDataFrameAnalyticsRow --
-- ------------------------------------------------------------------ --
describe "catMlDataFrameAnalyticsOptionsParams URI rendering" $
it "renders allow_no_match" $ do
let opts = defaultCatMlDataFrameAnalyticsOptions {cmfaoAllowNoMatch = Just False}
catMlDataFrameAnalyticsOptionsParams opts
`shouldContain` [("allow_no_match", Just "false")]
describe "CatMlDataFrameAnalyticsRow FromJSON" $
it "parses a documented row" $
case parseEither parseJSON =<< eitherDecode "[{\"id\":\"classifier_job_1\",\"type\":\"classification\",\"create_time\":\"2020-02-12T11:49:09.594Z\",\"state\":\"stopped\"}]" :: Either String [CatMlDataFrameAnalyticsRow] of
Right (row : _) -> do
cmfaId row `shouldBe` Just "classifier_job_1"
cmfaType row `shouldBe` Just "classification"
cmfaState row `shouldBe` Just "stopped"
cmfaCreateTime row `shouldBe` Just "2020-02-12T11:49:09.594Z"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catMlDatafeedsOptionsParams / CatMlDatafeedsRow --
-- ------------------------------------------------------------------ --
describe "catMlDatafeedsOptionsParams URI rendering" $
it "renders sort" $ do
let opts = defaultCatMlDatafeedsOptions {cmdfoSort = Just ("id" :| [])}
catMlDatafeedsOptionsParams opts
`shouldContain` [("s", Just "id")]
describe "CatMlDatafeedsRow FromJSON" $
it "parses dotted search/buckets keys" $
case parseEither parseJSON =<< eitherDecode "[{\"id\":\"datafeed-high_sum_total_sales\",\"state\":\"stopped\",\"buckets.count\":\"743\",\"search.count\":\"7\"}]" :: Either String [CatMlDatafeedsRow] of
Right (row : _) -> do
cmdfId row `shouldBe` Just "datafeed-high_sum_total_sales"
cmdfBucketsCount row `shouldBe` Just 743
cmdfSearchCount row `shouldBe` Just 7
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catMlTrainedModelsOptionsParams / CatMlTrainedModelsRow --
-- ------------------------------------------------------------------ --
describe "catMlTrainedModelsOptionsParams URI rendering" $ do
it "renders from/size pagination" $ do
let opts =
defaultCatMlTrainedModelsOptions
{ cmtoFrom = Just 10,
cmtoSize = Just 25
}
let ps = catMlTrainedModelsOptionsParams opts
ps `shouldContain` [("from", Just "10")]
ps `shouldContain` [("size", Just "25")]
describe "CatMlTrainedModelsRow FromJSON" $
it "parses a documented row including the __none__ sentinel" $
case parseEither parseJSON =<< eitherDecode "[{\"id\":\"lang_ident_model_1\",\"heap_size\":\"1mb\",\"operations\":\"39629\",\"create_time\":\"2019-12-05T12:28:34.594Z\",\"type\":\"lang_ident\",\"ingest.pipelines\":\"0\",\"data_frame.id\":\"__none__\"}]" :: Either String [CatMlTrainedModelsRow] of
Right (row : _) -> do
ctmrId row `shouldBe` Just "lang_ident_model_1"
ctmrHeapSize row `shouldBe` Just "1mb"
ctmrType row `shouldBe` Just "lang_ident"
ctmrIngestPipelines row `shouldBe` Just "0"
ctmrDataFrameId row `shouldBe` Just "__none__"
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catTransformsOptionsParams / CatTransformsRow --
-- ------------------------------------------------------------------ --
describe "catTransformsOptionsParams URI rendering" $
it "renders from/size pagination" $ do
let opts = defaultCatTransformsOptions {ctxoSize = Just 50}
catTransformsOptionsParams opts
`shouldContain` [("size", Just "50")]
describe "CatTransformsRow FromJSON" $ do
it "parses a documented row" $
case parseEither parseJSON =<< eitherDecode "[{\"id\":\"ecommerce_transform\",\"state\":\"started\",\"checkpoint\":\"1\",\"documents_processed\":\"705\",\"checkpoint_progress\":\"100.00\",\"changes_last_detection_time\":null}]" :: Either String [CatTransformsRow] of
Right (row : _) -> do
ctxrId row `shouldBe` Just "ecommerce_transform"
ctxrState row `shouldBe` Just "started"
ctxrCheckpoint row `shouldBe` Just "1"
ctxrDocumentsProcessed row `shouldBe` Just 705
ctxrCheckpointProgress row `shouldBe` Just "100.00"
ctxrChangesLastDetectionTime row `shouldBe` Nothing
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
it "maps a native JSON null last_search_time to Nothing" $
case parseEither parseJSON =<< eitherDecode "[{\"id\":\"t\",\"last_search_time\":null}]" :: Either String [CatTransformsRow] of
Right (row : _) -> ctxrLastSearchTime row `shouldBe` Nothing
Right [] -> expectationFailure "expected at least one row"
Left e -> expectationFailure ("expected parse, got " <> e)
-- ------------------------------------------------------------------ --
-- catHelp: plain-text meta endpoint (no params, non-JSON body) --
-- ------------------------------------------------------------------ --
describe "catHelp endpoint shape" $ do
let path req = getRawEndpoint (bhRequestEndpoint req)
queries req = getRawEndpointQueries (bhRequestEndpoint req)
it "targets /_cat (the cross-version help root)" $
path catHelp `shouldBe` ["_cat"]
it "carries no query string" $
queries catHelp `shouldBe` []
describe "catHelp integration" $
it "returns a non-empty plain-text help listing" $
withTestEnv $ do
help <- Client.catHelp
liftIO $ help `shouldSatisfy` (not . T.null)
where
-- \| Create a freshly-emptied cat-test index, run the action with
-- the index name, and delete the index afterwards even if the
-- action throws (so a failing assertion cannot leak the index into
-- subsequent runs).
withCatIndex :: IndexName -> (IndexName -> BH IO a) -> BH IO a
withCatIndex idx action = bracket_ setup teardown (action idx)
where
setup = do
_ <- tryPerformBHRequest $ deleteIndex idx
void $
performBHRequest $
createIndex
(IndexSettings (ShardCount 1) (ReplicaCount 0) defaultIndexMappingsLimits)
idx
teardown = void $ tryPerformBHRequest $ deleteIndex idx
-- \| Create a freshly-emptied source index and a write-index alias on
-- top of it, run the action with both names, and tear the alias and
-- the index down afterwards even if the action throws. Mirrors
-- 'withCatIndex' but additionally wires up 'createIndexAlias' /
-- 'deleteIndexAlias' so the @_cat\/aliases@ smoke test always has a
-- concrete alias to look for.
withCatAlias :: IndexName -> IndexName -> (IndexName -> IndexName -> BH IO a) -> BH IO a
withCatAlias idx alias action =
bracket_ setup teardown (action idx alias)
where
aliasName = IndexAliasName alias
writeCreate = defaultIndexAliasCreate {aliasCreateIsWriteIndex = Just True}
setup = do
_ <- tryPerformBHRequest $ deleteIndexAlias aliasName
_ <- tryPerformBHRequest $ deleteIndex idx
void $
performBHRequest $
createIndex
(IndexSettings (ShardCount 1) (ReplicaCount 0) defaultIndexMappingsLimits)
idx
void $ performBHRequest $ createIndexAlias idx aliasName writeCreate
teardown = do
_ <- tryPerformBHRequest $ deleteIndexAlias aliasName
void $ tryPerformBHRequest $ deleteIndex idx
-- \| Create a freshly-emptied cat-test index whose mapping enables
-- fielddata on a text field, index a single document populating that
-- field, and refresh. The fielddata cache itself is loaded lazily by
-- the caller (via a sort on the field); the helper only sets up the
-- precondition. Mirrors 'withCatIndex' / 'withCatAlias'.
withCatFielddata :: IndexName -> FieldName -> (IndexName -> FieldName -> BH IO a) -> BH IO a
withCatFielddata idx field action =
bracket_ setup teardown (action idx field)
where
setup = do
_ <- tryPerformBHRequest $ deleteIndex idx
void $
performBHRequest $
createIndex
(IndexSettings (ShardCount 1) (ReplicaCount 0) defaultIndexMappingsLimits)
idx
void $
performBHRequest $
putMapping @Value idx mappingBody
void $
performBHRequest $
indexDocument idx defaultIndexDocumentSettings docBody (DocId "1")
void $ performBHRequest $ refreshIndex idx
teardown = void $ tryPerformBHRequest $ deleteIndex idx
mappingBody =
object
[ "properties"
.= object
[ fromText (unFieldName field)
.= object
[ "type" .= ("text" :: Text),
"fielddata" .= True
]
]
]
docBody = object [fromText (unFieldName field) .= ("hello world" :: Text)]