bloodhound-1.0.0.0: tests/Test/DataStreamsSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Test.DataStreamsSpec (spec) where
import Data.ByteString.Lazy.Char8 qualified as LBS
import Data.Int (Int64)
import Data.List (isInfixOf, sort)
import Data.Map.Strict qualified as M
import Database.Bloodhound.ElasticSearch7.Requests qualified as ES7
import Database.Bloodhound.ElasticSearch8.Requests qualified as ES8
import Database.Bloodhound.ElasticSearch9.Requests qualified as ES9
import TestsUtils.Import
import Prelude
-- | Fixture mimicking the @GET /_data_stream@ response (two streams).
-- Adapted from the shape documented at
-- https://www.elastic.co/guide/en/elasticsearch/reference/current/data-stream-apis.html#get-data-streams.
sampleListResponse :: LBS.ByteString
sampleListResponse =
"{\
\ \"data_streams\": [\
\ {\
\ \"name\": \"logs-foobar\",\
\ \"timestamp_field\": { \"name\": \"@timestamp\" },\
\ \"indices\": [\
\ { \"index_name\": \".ds-logs-foobar-2024.01.01-000001\", \"index_uuid\": \"uuid-a\" }\
\ ],\
\ \"generation\": 1,\
\ \"status\": \"GREEN\",\
\ \"template\": \"logs-foobar-template\",\
\ \"hidden\": false\
\ },\
\ {\
\ \"name\": \"metrics-hidden\",\
\ \"timestamp_field\": { \"name\": \"ts\" },\
\ \"indices\": [],\
\ \"generation\": 5,\
\ \"status\": \"YELLOW\",\
\ \"template\": \"metrics-template\",\
\ \"ilm_policy\": \"logs-policy\",\
\ \"hidden\": true\
\ }\
\ ]\
\}"
-- | Fixture mimicking the @GET /_data_stream@ response when a stream is
-- missing the optional @ilm_policy@ field (the common case for streams
-- not backed by ILM).
sampleNoIlmPolicyResponse :: LBS.ByteString
sampleNoIlmPolicyResponse =
"{\
\ \"data_streams\": [\
\ {\
\ \"name\": \"plain-stream\",\
\ \"timestamp_field\": { \"name\": \"@timestamp\" },\
\ \"indices\": [],\
\ \"generation\": 1,\
\ \"status\": \"GREEN\",\
\ \"template\": \"plain-template\",\
\ \"hidden\": false\
\ }\
\ ]\
\}"
streamName :: DataStream -> Text
streamName = (\(DataStreamName n) -> n) . dataStreamName
-- | Construct an 'IndexName' at the value level for tests. Uses
-- 'mkIndexNameSystem' (not the stricter 'mkIndexName') because backing
-- index names start with @.@ (e.g. @.ds-stream-2024.01.01-000001@),
-- which the value-level 'FromJSON' instance accepts but 'qqIndexName'
-- rejects.
mkIdx :: Text -> IndexName
mkIdx = either (error . ("bad IndexName: " <>) . show) id . mkIndexNameSystem
-- | Build a minimal 'ExplainDataStreamLifecycleIndex' for round-trip
-- tests: only the required fields (@index@ and @managed_by_lifecycle@)
-- are populated; optionals are 'Nothing'.
minimalExplainEntry :: IndexName -> ExplainDataStreamLifecycleIndex
minimalExplainEntry name =
ExplainDataStreamLifecycleIndex
{ explainDataStreamLifecycleIndexIndex = name,
explainDataStreamLifecycleIndexManagedByLifecycle = True,
explainDataStreamLifecycleIndexCreationDateMillis = Nothing,
explainDataStreamLifecycleIndexTimeSinceIndexCreation = Nothing,
explainDataStreamLifecycleIndexRolloverDateMillis = Nothing,
explainDataStreamLifecycleIndexTimeSinceRollover = Nothing,
explainDataStreamLifecycleIndexLifecycle = Nothing,
explainDataStreamLifecycleIndexGenerationTime = Nothing,
explainDataStreamLifecycleIndexError = Nothing
}
-- | Fixture mimicking the @GET /_data_stream/_stats@ response with a
-- single data stream. Adapted from the shape documented at
-- https://www.elastic.co/guide/en/elasticsearch/reference/current/data-stream-apis.html#get-data-stream-stats.
sampleStatsResponse :: LBS.ByteString
sampleStatsResponse =
"{\
\ \"_shards\": { \"total\": 2, \"successful\": 2, \"skipped\": 0, \"failed\": 0 },\
\ \"data_stream_count\": 1,\
\ \"backing_indices\": 2,\
\ \"total_store_size_bytes\": 786432,\
\ \"total_store_size\": \"786kb\",\
\ \"data_streams\": [\
\ {\
\ \"data_stream\": \"logs-foobar\",\
\ \"backing_indices\": 2,\
\ \"store_size_bytes\": 786432,\
\ \"store_size\": \"786kb\",\
\ \"maximum_timestamp\": 1716854400000\
\ }\
\ ]\
\}"
spec :: Spec
spec = describe "Data Streams API" $ do
describe "DataStream JSON" $ do
it "decodes a single stream with all fields populated" $ do
let decoded = decode "{ \"name\": \"ds\", \"timestamp_field\": { \"name\": \"@timestamp\" }, \"indices\": [], \"generation\": 1, \"status\": \"GREEN\", \"template\": \"tpl\", \"hidden\": false }" :: Maybe DataStream
streamName <$> decoded `shouldBe` Just "ds"
dataStreamGeneration <$> decoded `shouldBe` Just 1
dataStreamStatus <$> decoded `shouldBe` Just Green
dataStreamHidden <$> decoded `shouldBe` Just False
dataStreamIlmPolicy <$> decoded `shouldBe` Just Nothing
it "decodes ilm_policy when present" $ do
let decoded = decode "{ \"name\": \"ds\", \"timestamp_field\": { \"name\": \"@timestamp\" }, \"indices\": [], \"generation\": 1, \"status\": \"GREEN\", \"template\": \"tpl\", \"ilm_policy\": \"my-policy\", \"hidden\": true }" :: Maybe DataStream
dataStreamIlmPolicy <$> decoded `shouldBe` Just (Just "my-policy")
dataStreamHidden <$> decoded `shouldBe` Just True
it "round-trips through ToJSON/FromJSON" $ do
let Just decoded = decode "{ \"name\": \"ds\", \"timestamp_field\": { \"name\": \"@timestamp\" }, \"indices\": [], \"generation\": 1, \"status\": \"GREEN\", \"template\": \"tpl\", \"hidden\": false }" :: Maybe DataStream
(decode . encode) decoded `shouldBe` Just decoded
it "decodes system/replicated/_meta when present" $ do
let decoded = decode "{ \"name\": \"ds\", \"timestamp_field\": { \"name\": \"@timestamp\" }, \"indices\": [], \"generation\": 1, \"status\": \"GREEN\", \"template\": \"tpl\", \"hidden\": false, \"system\": true, \"replicated\": false, \"_meta\": { \"my-meta-field\": \"foo\" } }" :: Maybe DataStream
expectedMeta = decode "{ \"my-meta-field\": \"foo\" }" :: Maybe Object
dataStreamSystem <$> decoded `shouldBe` Just (Just True)
dataStreamReplicated <$> decoded `shouldBe` Just (Just False)
dataStreamMeta <$> decoded `shouldBe` Just expectedMeta
it "rejects a non-object _meta (object is the documented wire type)" $ do
let decoded = decode "{ \"name\": \"ds\", \"timestamp_field\": { \"name\": \"@timestamp\" }, \"indices\": [], \"generation\": 1, \"status\": \"GREEN\", \"template\": \"tpl\", \"hidden\": false, \"_meta\": \"not-an-object\" }" :: Maybe DataStream
decoded `shouldBe` Nothing
it "decodes absent system/replicated/_meta as Nothing" $ do
let decoded = decode "{ \"name\": \"ds\", \"timestamp_field\": { \"name\": \"@timestamp\" }, \"indices\": [], \"generation\": 1, \"status\": \"GREEN\", \"template\": \"tpl\", \"hidden\": false }" :: Maybe DataStream
dataStreamSystem <$> decoded `shouldBe` Just Nothing
dataStreamReplicated <$> decoded `shouldBe` Just Nothing
dataStreamMeta <$> decoded `shouldBe` Just Nothing
it "decodes explicit null system/replicated/_meta as Nothing" $ do
let decoded = decode "{ \"name\": \"ds\", \"timestamp_field\": { \"name\": \"@timestamp\" }, \"indices\": [], \"generation\": 1, \"status\": \"GREEN\", \"template\": \"tpl\", \"hidden\": false, \"system\": null, \"replicated\": null, \"_meta\": null }" :: Maybe DataStream
dataStreamSystem <$> decoded `shouldBe` Just Nothing
dataStreamReplicated <$> decoded `shouldBe` Just Nothing
dataStreamMeta <$> decoded `shouldBe` Just Nothing
it "round-trips a fully-populated stream through ToJSON/FromJSON" $ do
let Just decoded = decode "{ \"name\": \"ds\", \"timestamp_field\": { \"name\": \"@timestamp\" }, \"indices\": [], \"generation\": 1, \"status\": \"GREEN\", \"template\": \"tpl\", \"ilm_policy\": \"my-policy\", \"hidden\": false, \"system\": true, \"replicated\": false, \"_meta\": { \"my-meta-field\": \"foo\" } }" :: Maybe DataStream
(decode . encode) decoded `shouldBe` Just decoded
it "preserves system/replicated/_meta as Nothing on the existing two-stream fixture" $ do
let Just decoded = decode sampleListResponse :: Maybe GetDataStreamsResponse
[s1, s2] = dataStreams decoded
dataStreamSystem s1 `shouldBe` Nothing
dataStreamReplicated s1 `shouldBe` Nothing
dataStreamMeta s1 `shouldBe` Nothing
dataStreamSystem s2 `shouldBe` Nothing
dataStreamReplicated s2 `shouldBe` Nothing
dataStreamMeta s2 `shouldBe` Nothing
it "rejects a body missing the required 'name' field" $ do
let decoded = decode "{ \"generation\": 1 }" :: Maybe DataStream
decoded `shouldBe` Nothing
it "decodes a stream carrying a lowercase / degenerate status through the full record" $ do
let decoded = decode "{ \"name\": \"ds\", \"timestamp_field\": { \"name\": \"@timestamp\" }, \"indices\": [], \"generation\": 1, \"status\": \"unknown\", \"template\": \"tpl\", \"hidden\": false }" :: Maybe DataStream
dataStreamStatus <$> decoded `shouldBe` Just Unknown
it "decodes DataStreamStatus variants (uppercase)" $ do
decode "\"GREEN\"" `shouldBe` Just Green
decode "\"YELLOW\"" `shouldBe` Just Yellow
decode "\"RED\"" `shouldBe` Just Red
it "decodes DataStreamStatus variants (lowercase)" $ do
decode "\"green\"" `shouldBe` Just Green
decode "\"yellow\"" `shouldBe` Just Yellow
decode "\"red\"" `shouldBe` Just Red
it "decodes unknown/unavailable health states" $ do
decode "\"unknown\"" `shouldBe` Just Unknown
decode "\"unavailable\"" `shouldBe` Just Unavailable
it "round-trips Unknown and Unavailable" $ do
(decode . encode) Unknown `shouldBe` Just Unknown
(decode . encode) Unavailable `shouldBe` Just Unavailable
it "still rejects truly invalid status values" $
(decode "\"purple\"" :: Maybe DataStreamStatus) `shouldBe` Nothing
describe "GetDataStreamsResponse JSON" $ do
it "decodes the list-all response with two streams" $ do
let Just decoded = decode sampleListResponse :: Maybe GetDataStreamsResponse
length (dataStreams decoded) `shouldBe` 2
sort (map streamName (dataStreams decoded))
`shouldBe` ["logs-foobar", "metrics-hidden"]
it "preserves ilm_policy on the stream that has one" $ do
let Just decoded = decode sampleListResponse :: Maybe GetDataStreamsResponse
hiddenStream = head [s | s <- dataStreams decoded, streamName s == "metrics-hidden"]
dataStreamIlmPolicy hiddenStream `shouldBe` Just "logs-policy"
dataStreamHidden hiddenStream `shouldBe` True
it "decodes the response missing ilm_policy as Nothing" $ do
let Just decoded = decode sampleNoIlmPolicyResponse :: Maybe GetDataStreamsResponse
[plainStream] = dataStreams decoded
dataStreamIlmPolicy plainStream `shouldBe` Nothing
it "decodes an empty data_streams array as the empty list" $ do
let decoded = decode "{ \"data_streams\": [] }" :: Maybe GetDataStreamsResponse
(dataStreams <$> decoded) `shouldBe` Just []
it "rejects a top-level array (the response must be a keyed object)" $ do
let decoded = decode "[]" :: Maybe GetDataStreamsResponse
decoded `shouldBe` Nothing
it "rejects malformed JSON" $ do
let decoded = decode "{ this is not json" :: Maybe GetDataStreamsResponse
decoded `shouldBe` Nothing
describe "getDataStreams endpoint shape" $ do
-- Pure checks against the BHRequest shape; no live backend needed.
it "GETs /_data_stream when given Nothing" $ do
let req = ES7.getDataStreams Nothing
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "GETs /_data_stream when given Just [] (empty list collapses to all)" $ do
let req = ES7.getDataStreams (Just [])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "GETs /_data_stream/{name} when given a single-element list" $ do
let req = ES7.getDataStreams (Just [DataStreamName "logs-foobar"])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "logs-foobar"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "GETs /_data_stream/{a,b,c} when given a multi-element list (comma-joined)" $ do
let req = ES7.getDataStreams (Just [DataStreamName "a", DataStreamName "b", DataStreamName "c"])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "a,b,c"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "does not attach a body (GET semantics)" $ do
let req = ES7.getDataStreams Nothing
bhRequestBody req `shouldBe` Nothing
describe "createDataStream endpoint shape" $ do
-- Pure checks against the BHRequest shape; no live backend needed.
it "PUTs /_data_stream/{name} for the given data stream" $ do
let req = ES7.createDataStream (DataStreamName "logs-foobar")
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "logs-foobar"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "uses the PUT method" $ do
let req = ES7.createDataStream (DataStreamName "logs-foobar")
bhRequestMethod req `shouldBe` "PUT"
it "sends an empty body (ES derives the config from the index template)" $ do
let req = ES7.createDataStream (DataStreamName "logs-foobar")
bhRequestBody req `shouldBe` Just ""
describe "deleteDataStream endpoint shape" $ do
-- Pure checks against the BHRequest shape; no live backend needed.
it "DELETEs /_data_stream/{name} for the given data stream" $ do
let req = ES7.deleteDataStream (DataStreamName "logs-foobar")
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "logs-foobar"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "uses the DELETE method" $ do
let req = ES7.deleteDataStream (DataStreamName "logs-foobar")
bhRequestMethod req `shouldBe` "DELETE"
it "does not attach a body (DELETE semantics)" $ do
let req = ES7.deleteDataStream (DataStreamName "logs-foobar")
bhRequestBody req `shouldBe` Nothing
describe "migrateToDataStream endpoint shape" $ do
-- Pure checks against the BHRequest shape; no live backend needed.
it "POSTs /_data_stream/_migrate/{alias} for the given alias" $ do
let req = ES7.migrateToDataStream (IndexAliasName [qqIndexName|logs-alias|])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "_migrate", "logs-alias"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "uses the POST method" $ do
let req = ES7.migrateToDataStream (IndexAliasName [qqIndexName|logs-alias|])
bhRequestMethod req `shouldBe` "POST"
it "sends an empty body (ES derives the config from the alias and its write index)" $ do
let req = ES7.migrateToDataStream (IndexAliasName [qqIndexName|logs-alias|])
bhRequestBody req `shouldBe` Just ""
describe "promoteDataStream endpoint shape" $ do
-- Pure checks against the BHRequest shape; no live backend needed.
it "POSTs /_data_stream/_promote/{name} for the given data stream" $ do
let req = ES7.promoteDataStream (DataStreamName "logs-foobar")
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "_promote", "logs-foobar"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "uses the POST method" $ do
let req = ES7.promoteDataStream (DataStreamName "logs-foobar")
bhRequestMethod req `shouldBe` "POST"
it "sends an empty body (ES promotes the stream in place)" $ do
let req = ES7.promoteDataStream (DataStreamName "logs-foobar")
bhRequestBody req `shouldBe` Just ""
describe "DataStreamStat JSON" $ do
it "decodes a stat with all fields populated" $ do
let decoded = decode "{ \"data_stream\": \"logs-foobar\", \"backing_indices\": 2, \"store_size_bytes\": 786432, \"store_size\": \"786kb\", \"maximum_timestamp\": 1716854400000 }" :: Maybe DataStreamStat
(dataStreamStatName <$> decoded) `shouldBe` Just (DataStreamName "logs-foobar")
(dataStreamStatBackingIndices <$> decoded) `shouldBe` Just 2
(dataStreamStatStoreSizeBytes <$> decoded) `shouldBe` Just (786432 :: Int64)
(dataStreamStatStoreSize <$> decoded) `shouldBe` Just (Just "786kb")
(dataStreamStatMaximumTimestamp <$> decoded) `shouldBe` Just 1716854400000
it "round-trips through ToJSON/FromJSON" $ do
let Just decoded = decode "{ \"data_stream\": \"ds\", \"backing_indices\": 1, \"store_size_bytes\": 1024, \"store_size\": \"1kb\", \"maximum_timestamp\": 1700000000 }" :: Maybe DataStreamStat
(decode . encode) decoded `shouldBe` Just decoded
it "rejects a body missing the required 'data_stream' field" $ do
let decoded = decode "{ \"backing_indices\": 1 }" :: Maybe DataStreamStat
decoded `shouldBe` Nothing
it "rejects a per-stream stat using 'name' instead of 'data_stream'" $ do
let decoded = decode "{ \"name\": \"logs-foobar\", \"backing_indices\": 2, \"store_size_bytes\": 786432, \"store_size\": \"786kb\", \"maximum_timestamp\": 1716854400000 }" :: Maybe DataStreamStat
decoded `shouldBe` Nothing
it "decodes a stat missing the human-only 'store_size' as Nothing (human=false)" $ do
let decoded = decode "{ \"data_stream\": \"ds\", \"backing_indices\": 1, \"store_size_bytes\": 1024, \"maximum_timestamp\": 1700000000 }" :: Maybe DataStreamStat
dataStreamStatStoreSize <$> decoded `shouldBe` Just Nothing
(dataStreamStatStoreSizeBytes <$> decoded) `shouldBe` Just (1024 :: Int64)
it "decodes a stat whose 'store_size' is explicitly null as Nothing" $ do
let decoded = decode "{ \"data_stream\": \"ds\", \"backing_indices\": 1, \"store_size_bytes\": 1024, \"store_size\": null, \"maximum_timestamp\": 1700000000 }" :: Maybe DataStreamStat
dataStreamStatStoreSize <$> decoded `shouldBe` Just Nothing
it "round-trips a stat without store_size (encodes the field as null)" $ do
let Just decoded = decode "{ \"data_stream\": \"ds\", \"backing_indices\": 1, \"store_size_bytes\": 1024, \"maximum_timestamp\": 1700000000 }" :: Maybe DataStreamStat
(decode . encode) decoded `shouldBe` Just decoded
LBS.unpack (encode decoded) `shouldSatisfy` isInfixOf "\"store_size\":null"
describe "DataStreamStats JSON" $ do
it "decodes the full response with one data stream" $ do
let Just decoded = decode sampleStatsResponse :: Maybe DataStreamStats
dataStreamStatsCount decoded `shouldBe` 1
dataStreamStatsBackingIndices decoded `shouldBe` 2
dataStreamStatsTotalStoreSizeBytes decoded `shouldBe` (786432 :: Int64)
dataStreamStatsTotalStoreSize decoded `shouldBe` Just "786kb"
length (dataStreamStatsDataStreams decoded) `shouldBe` 1
it "decodes the _shards summary into a ShardResult" $ do
let Just decoded = decode sampleStatsResponse :: Maybe DataStreamStats
dataStreamStatsShards decoded `shouldBe` ShardResult 2 2 0 0
it "preserves per-stream fields through decode" $ do
let Just decoded = decode sampleStatsResponse :: Maybe DataStreamStats
[stat] = dataStreamStatsDataStreams decoded
dataStreamStatName stat `shouldBe` DataStreamName "logs-foobar"
dataStreamStatStoreSize stat `shouldBe` Just "786kb"
dataStreamStatMaximumTimestamp stat `shouldBe` 1716854400000
it "decodes multiple data streams in the data_streams array" $ do
let payload =
"{ \"_shards\": { \"total\": 4, \"successful\": 4, \"skipped\": 0, \"failed\": 0 }"
<> ", \"data_stream_count\": 2, \"backing_indices\": 4, \"total_store_size_bytes\": 1000, \"total_store_size\": \"1kb\""
<> ", \"data_streams\": ["
<> " { \"data_stream\": \"a\", \"backing_indices\": 2, \"store_size_bytes\": 500, \"store_size\": \"500b\", \"maximum_timestamp\": 1 },"
<> " { \"data_stream\": \"b\", \"backing_indices\": 2, \"store_size_bytes\": 500, \"store_size\": \"500b\", \"maximum_timestamp\": 2 }"
<> "] }"
Just decoded = decode payload :: Maybe DataStreamStats
length (dataStreamStatsDataStreams decoded) `shouldBe` 2
sort (map (\s -> (\(DataStreamName n) -> n) (dataStreamStatName s)) (dataStreamStatsDataStreams decoded))
`shouldBe` ["a", "b"]
it "round-trips through ToJSON/FromJSON" $ do
let Just decoded = decode sampleStatsResponse :: Maybe DataStreamStats
(decode . encode) decoded `shouldBe` Just decoded
it "decodes an empty data_streams array as the empty list" $ do
let decoded = decode "{ \"_shards\": { \"total\": 0, \"successful\": 0, \"skipped\": 0, \"failed\": 0 }, \"data_stream_count\": 0, \"backing_indices\": 0, \"total_store_size_bytes\": 0, \"total_store_size\": \"0b\", \"data_streams\": [] }" :: Maybe DataStreamStats
(dataStreamStatsDataStreams <$> decoded) `shouldBe` Just []
it "rejects a response missing the _shards field" $ do
let decoded = decode "{ \"data_stream_count\": 0, \"backing_indices\": 0, \"total_store_size_bytes\": 0, \"total_store_size\": \"0b\", \"data_streams\": [] }" :: Maybe DataStreamStats
decoded `shouldBe` Nothing
it "decodes a response missing the human-only 'total_store_size' as Nothing (human=false)" $ do
let payload =
"{ \"_shards\": { \"total\": 1, \"successful\": 1, \"skipped\": 0, \"failed\": 0 }"
<> ", \"data_stream_count\": 0, \"backing_indices\": 0, \"total_store_size_bytes\": 0, \"data_streams\": [] }"
Just decoded = decode payload :: Maybe DataStreamStats
dataStreamStatsTotalStoreSize decoded `shouldBe` Nothing
dataStreamStatsTotalStoreSizeBytes decoded `shouldBe` (0 :: Int64)
it "round-trips a response without total_store_size (encodes the field as null)" $ do
let payload =
"{ \"_shards\": { \"total\": 1, \"successful\": 1, \"skipped\": 0, \"failed\": 0 }"
<> ", \"data_stream_count\": 0, \"backing_indices\": 0, \"total_store_size_bytes\": 0, \"data_streams\": [] }"
Just decoded = decode payload :: Maybe DataStreamStats
(decode . encode) decoded `shouldBe` Just decoded
LBS.unpack (encode decoded) `shouldSatisfy` isInfixOf "\"total_store_size\":null"
it "rejects a top-level array (the response must be a keyed object)" $ do
let decoded = decode "[]" :: Maybe DataStreamStats
decoded `shouldBe` Nothing
it "rejects malformed JSON" $ do
let decoded = decode "{ this is not json" :: Maybe DataStreamStats
decoded `shouldBe` Nothing
describe "getDataStreamStats endpoint shape" $ do
-- Pure checks against the BHRequest shape; no live backend needed.
it "GETs /_data_stream/_stats when given Nothing" $ do
let req = ES7.getDataStreamStats Nothing
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "_stats"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "GETs /_data_stream/_stats when given Just [] (empty list collapses to all)" $ do
let req = ES7.getDataStreamStats (Just [])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "_stats"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "GETs /_data_stream/{name}/_stats when given a single-element list" $ do
let req = ES7.getDataStreamStats (Just [DataStreamName "logs-foobar"])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "logs-foobar", "_stats"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "GETs /_data_stream/{a,b,c}/_stats when given a multi-element list (comma-joined)" $ do
let req = ES7.getDataStreamStats (Just [DataStreamName "a", DataStreamName "b", DataStreamName "c"])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "a,b,c", "_stats"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "does not attach a body (GET semantics)" $ do
let req = ES7.getDataStreamStats Nothing
bhRequestBody req `shouldBe` Nothing
-- ============================================================
-- Data Stream Lifecycle (ES 8.x+, back-filled to ES 7.17.x)
-- ============================================================
describe "DataStreamLifecycleDownsampling JSON" $ do
it "decodes a fully-populated downsampling entry" $ do
let decoded = decode "{ \"after\": \"10d\", \"fixed_interval\": \"1h\" }" :: Maybe DataStreamLifecycleDownsampling
dataStreamLifecycleDownsamplingAfter <$> decoded `shouldBe` Just (Just "10d")
dataStreamLifecycleDownsamplingFixedInterval <$> decoded `shouldBe` Just (Just "1h")
it "decodes an empty object as both fields Nothing" $ do
let decoded = decode "{}" :: Maybe DataStreamLifecycleDownsampling
decoded `shouldBe` Just (DataStreamLifecycleDownsampling Nothing Nothing)
it "round-trips through ToJSON/FromJSON" $ do
let Just decoded = decode "{ \"after\": \"10d\", \"fixed_interval\": \"1h\" }" :: Maybe DataStreamLifecycleDownsampling
(decode . encode) decoded `shouldBe` Just decoded
it "omits Nothing fields on encode (clean wire form)" $ do
let value = DataStreamLifecycleDownsampling (Just "10d") Nothing
encode value `shouldBe` "{\"after\":\"10d\"}"
describe "SamplingMethod JSON (data-stream downsampling method)" $ do
it "decodes aggregate" $ decode "\"aggregate\"" `shouldBe` Just Aggregate
it "decodes last_value" $ decode "\"last_value\"" `shouldBe` Just LastValue
it "rejects an unknown method" $ (decode "\"average\"" :: Maybe SamplingMethod) `shouldBe` Nothing
it "round-trips both variants" $ do
(decode . encode) Aggregate `shouldBe` Just Aggregate
(decode . encode) LastValue `shouldBe` Just LastValue
describe "DataStreamLifecycle JSON" $ do
it "decodes a fully-populated lifecycle" $ do
let payload =
"{ \"data_retention\": \"30d\""
<> ", \"enabled\": true"
<> ", \"downsampling\": [{ \"after\": \"10d\", \"fixed_interval\": \"1h\" }]"
<> ", \"downsampling_method\": \"aggregate\" }"
Just decoded = decode payload :: Maybe DataStreamLifecycle
dataStreamLifecycleDataRetention decoded `shouldBe` Just "30d"
dataStreamLifecycleEnabled decoded `shouldBe` Just True
dataStreamLifecycleDownsampling decoded `shouldBe` Just [DataStreamLifecycleDownsampling (Just "10d") (Just "1h")]
dataStreamLifecycleDownsamplingMethod decoded `shouldBe` Just Aggregate
it "decodes an empty object as the all-Nothing lifecycle" $ do
let decoded = decode "{}" :: Maybe DataStreamLifecycle
decoded `shouldBe` Just (DataStreamLifecycle Nothing Nothing Nothing Nothing Nothing Nothing)
it "decodes data_retention-only as the rest Nothing" $ do
let decoded = decode "{ \"data_retention\": \"7d\" }" :: Maybe DataStreamLifecycle
dataStreamLifecycleDataRetention <$> decoded `shouldBe` Just (Just "7d")
dataStreamLifecycleEnabled <$> decoded `shouldBe` Just Nothing
it "round-trips a fully-populated lifecycle" $ do
let Just decoded = decode "{ \"data_retention\": \"30d\", \"enabled\": true, \"downsampling\": [{ \"after\": \"10d\", \"fixed_interval\": \"1h\" }], \"downsampling_method\": \"aggregate\" }" :: Maybe DataStreamLifecycle
(decode . encode) decoded `shouldBe` Just decoded
it "omits Nothing fields on encode (clean PUT body)" $ do
let value = DataStreamLifecycle (Just "7d") Nothing Nothing Nothing Nothing Nothing
encode value `shouldBe` "{\"data_retention\":\"7d\"}"
it "encodes the all-Nothing lifecycle as an empty object" $ do
encode (DataStreamLifecycle Nothing Nothing Nothing Nothing Nothing Nothing) `shouldBe` "{}"
it "rejects a top-level array" $ do
let decoded = decode "[]" :: Maybe DataStreamLifecycle
decoded `shouldBe` Nothing
describe "DataStreamLifecycleInfo JSON" $ do
it "decodes a managed stream with a populated lifecycle" $ do
let payload =
"{ \"name\": \"logs-app\""
<> ", \"lifecycle\": { \"data_retention\": \"7d\", \"enabled\": true } }"
Just decoded = decode payload :: Maybe DataStreamLifecycleInfo
dataStreamLifecycleInfoName decoded `shouldBe` DataStreamName "logs-app"
dataStreamLifecycleInfoLifecycle decoded `shouldSatisfy` isJust
it "decodes an unmanaged stream with lifecycle:null as Nothing" $ do
let Just decoded = decode "{ \"name\": \"logs-app\", \"lifecycle\": null }" :: Maybe DataStreamLifecycleInfo
dataStreamLifecycleInfoName decoded `shouldBe` DataStreamName "logs-app"
dataStreamLifecycleInfoLifecycle decoded `shouldBe` Nothing
it "decodes an unmanaged stream with lifecycle absent as Nothing" $ do
let Just decoded = decode "{ \"name\": \"logs-app\" }" :: Maybe DataStreamLifecycleInfo
dataStreamLifecycleInfoLifecycle decoded `shouldBe` Nothing
it "round-trips a managed stream" $ do
let Just decoded = decode "{ \"name\": \"ds\", \"lifecycle\": { \"data_retention\": \"7d\" } }" :: Maybe DataStreamLifecycleInfo
(decode . encode) decoded `shouldBe` Just decoded
it "rejects an info missing the data_stream field" $ do
let decoded = decode "{ \"lifecycle\": { \"data_retention\": \"7d\" } }" :: Maybe DataStreamLifecycleInfo
decoded `shouldBe` Nothing
describe "GetDataStreamLifecyclesResponse JSON" $ do
it "decodes a response with two managed streams" $ do
let payload =
"{ \"data_streams\": ["
<> " { \"name\": \"a\", \"lifecycle\": { \"data_retention\": \"1d\" } },"
<> " { \"name\": \"b\", \"lifecycle\": { \"data_retention\": \"2d\" } } ] }"
Just decoded = decode payload :: Maybe GetDataStreamLifecyclesResponse
length (dataStreamLifecycles decoded) `shouldBe` 2
it "decodes a stream with lifecycle:null alongside a managed one" $ do
let payload =
"{ \"data_streams\": ["
<> " { \"name\": \"managed\", \"lifecycle\": { \"data_retention\": \"1d\" } },"
<> " { \"name\": \"unmanaged\", \"lifecycle\": null } ] }"
Just decoded = decode payload :: Maybe GetDataStreamLifecyclesResponse
length (dataStreamLifecycles decoded) `shouldBe` 2
let unmanaged = head [i | i <- dataStreamLifecycles decoded, (\(DataStreamName n) -> n) (dataStreamLifecycleInfoName i) == "unmanaged"]
dataStreamLifecycleInfoLifecycle unmanaged `shouldBe` Nothing
it "decodes an empty data_streams array" $ do
let decoded = decode "{ \"data_streams\": [] }" :: Maybe GetDataStreamLifecyclesResponse
(dataStreamLifecycles <$> decoded) `shouldBe` Just []
it "round-trips through ToJSON/FromJSON" $ do
let Just decoded = decode "{ \"data_streams\": [{ \"name\": \"a\", \"lifecycle\": { \"data_retention\": \"1d\" } }] }" :: Maybe GetDataStreamLifecyclesResponse
(decode . encode) decoded `shouldBe` Just decoded
it "rejects a response missing the data_streams field" $ do
let decoded = decode "{}" :: Maybe GetDataStreamLifecyclesResponse
decoded `shouldBe` Nothing
it "rejects a top-level array" $ do
let decoded = decode "[]" :: Maybe GetDataStreamLifecyclesResponse
decoded `shouldBe` Nothing
describe "putDataStreamLifecycle endpoint shape" $ do
it "PUTs /_data_stream/{name}/_lifecycle for the given stream" $ do
let req = ES8.putDataStreamLifecycle (DataStreamName "logs-foobar") (DataStreamLifecycle (Just "7d") Nothing Nothing Nothing Nothing Nothing)
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "logs-foobar", "_lifecycle"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "uses the PUT method" $ do
let req = ES8.putDataStreamLifecycle (DataStreamName "logs-foobar") (DataStreamLifecycle Nothing Nothing Nothing Nothing Nothing Nothing)
bhRequestMethod req `shouldBe` "PUT"
it "sends the encoded DataStreamLifecycle as the body" $ do
let req = ES8.putDataStreamLifecycle (DataStreamName "logs-foobar") (DataStreamLifecycle (Just "7d") Nothing Nothing Nothing Nothing Nothing)
bhRequestBody req `shouldBe` Just "{\"data_retention\":\"7d\"}"
it "sends an empty object body when the lifecycle is all-Nothing" $ do
let req = ES8.putDataStreamLifecycle (DataStreamName "logs-foobar") (DataStreamLifecycle Nothing Nothing Nothing Nothing Nothing Nothing)
bhRequestBody req `shouldBe` Just "{}"
describe "getDataStreamLifecycle endpoint shape" $ do
it "GETs /_data_stream/_lifecycle when given Nothing" $ do
let req = ES8.getDataStreamLifecycle Nothing
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "_lifecycle"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "GETs /_data_stream/_lifecycle when given Just [] (empty list collapses to all)" $ do
let req = ES8.getDataStreamLifecycle (Just [])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "_lifecycle"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "GETs /_data_stream/{name}/_lifecycle when given a single-element list" $ do
let req = ES8.getDataStreamLifecycle (Just [DataStreamName "logs-foobar"])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "logs-foobar", "_lifecycle"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "GETs /_data_stream/{a,b,c}/_lifecycle when given a multi-element list (comma-joined)" $ do
let req = ES8.getDataStreamLifecycle (Just [DataStreamName "a", DataStreamName "b", DataStreamName "c"])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "a,b,c", "_lifecycle"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "does not attach a body (GET semantics)" $ do
let req = ES8.getDataStreamLifecycle Nothing
bhRequestBody req `shouldBe` Nothing
describe "deleteDataStreamLifecycle endpoint shape" $ do
it "DELETEs /_data_stream/{name}/_lifecycle for the given stream" $ do
let req = ES8.deleteDataStreamLifecycle (DataStreamName "logs-foobar")
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "logs-foobar", "_lifecycle"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "uses the DELETE method" $ do
let req = ES8.deleteDataStreamLifecycle (DataStreamName "logs-foobar")
bhRequestMethod req `shouldBe` "DELETE"
it "does not attach a body (DELETE semantics)" $ do
let req = ES8.deleteDataStreamLifecycle (DataStreamName "logs-foobar")
bhRequestBody req `shouldBe` Nothing
describe "DataStreamLifecycle read-only fields (effective_retention, retention_determined_by)" $ do
it "decodes effective_retention on a GET response" $ do
let payload = "{ \"data_retention\": \"7d\", \"effective_retention\": \"7d\", \"retention_determined_by\": \"data_stream_configuration\" }"
Just decoded = decode payload :: Maybe DataStreamLifecycle
dataStreamLifecycleEffectiveRetention decoded `shouldBe` Just "7d"
dataStreamLifecycleRetentionDeterminedBy decoded `shouldBe` Just "data_stream_configuration"
it "decodes retention_determined_by=max_global_retention when global retention caps the stream" $ do
let Just decoded = decode "{ \"data_retention\": \"365d\", \"effective_retention\": \"30d\", \"retention_determined_by\": \"max_global_retention\" }" :: Maybe DataStreamLifecycle
dataStreamLifecycleRetentionDeterminedBy decoded `shouldBe` Just "max_global_retention"
it "round-trips a lifecycle with read-only fields populated" $ do
let Just decoded = decode "{ \"data_retention\": \"7d\", \"effective_retention\": \"7d\", \"retention_determined_by\": \"data_stream_configuration\" }" :: Maybe DataStreamLifecycle
(decode . encode) decoded `shouldBe` Just decoded
it "omits read-only fields on PUT when they are Nothing (clean wire form)" $ do
let value = DataStreamLifecycle (Just "7d") Nothing Nothing Nothing Nothing Nothing
encode value `shouldBe` "{\"data_retention\":\"7d\"}"
describe "DataStreamGlobalRetention JSON" $ do
it "decodes a fully-populated global_retention object" $ do
let Just decoded = decode "{ \"max_retention\": \"365d\", \"default_retention\": \"30d\" }" :: Maybe DataStreamGlobalRetention
dataStreamGlobalRetentionMaxRetention decoded `shouldBe` Just "365d"
dataStreamGlobalRetentionDefaultRetention decoded `shouldBe` Just "30d"
it "decodes an empty object as both fields Nothing" $ do
let decoded = decode "{}" :: Maybe DataStreamGlobalRetention
decoded `shouldBe` Just (DataStreamGlobalRetention Nothing Nothing)
it "round-trips through ToJSON/FromJSON" $ do
let Just decoded = decode "{ \"max_retention\": \"365d\", \"default_retention\": \"30d\" }" :: Maybe DataStreamGlobalRetention
(decode . encode) decoded `shouldBe` Just decoded
describe "S6: omitNulls collapse of empty downsampling list" $ do
-- Pins the documented behavior: Just [] and Nothing are indistinguishable
-- on the wire (both omit the field), because omitNulls drops empty arrays.
it "encodes Just [] downsampling the same as Nothing downsampling" $ do
let withEmptyList = DataStreamLifecycle Nothing Nothing (Just []) Nothing Nothing Nothing
withNothing = DataStreamLifecycle Nothing Nothing Nothing Nothing Nothing Nothing
encode withEmptyList `shouldBe` encode withNothing
encode withEmptyList `shouldBe` "{}"
describe "GetDataStreamLifecyclesResponse with global_retention" $ do
it "decodes a response carrying global_retention" $ do
let payload =
"{ \"data_streams\": [{ \"name\": \"a\", \"lifecycle\": { \"data_retention\": \"1d\" } }]"
<> ", \"global_retention\": { \"max_retention\": \"365d\", \"default_retention\": \"30d\" } }"
Just decoded = decode payload :: Maybe GetDataStreamLifecyclesResponse
length (dataStreamLifecycles decoded) `shouldBe` 1
dataStreamLifecyclesGlobalRetention decoded `shouldSatisfy` isJust
let Just gr = dataStreamLifecyclesGlobalRetention decoded
dataStreamGlobalRetentionMaxRetention gr `shouldBe` Just "365d"
dataStreamGlobalRetentionDefaultRetention gr `shouldBe` Just "30d"
it "decodes a response without global_retention as Nothing" $ do
let Just decoded = decode "{ \"data_streams\": [] }" :: Maybe GetDataStreamLifecyclesResponse
dataStreamLifecyclesGlobalRetention decoded `shouldBe` Nothing
it "round-trips a response with global_retention" $ do
let payload =
"{ \"data_streams\": [{ \"name\": \"a\", \"lifecycle\": { \"data_retention\": \"1d\" } }]"
<> ", \"global_retention\": { \"max_retention\": \"365d\", \"default_retention\": \"30d\" } }"
Just decoded = decode payload :: Maybe GetDataStreamLifecyclesResponse
(decode . encode) decoded `shouldBe` Just decoded
describe "GET /_data_stream/{name}/_lifecycle verbatim ES docs example" $ do
-- Anchor: payload lifted verbatim from the ES 8.x REST docs page
-- operation-indices-get-data-lifecycle response example.
it "decodes the two-stream example from the official docs" $ do
let docsExample =
"{ \"data_streams\": ["
<> " { \"name\": \"my-data-stream-1\", \"lifecycle\": { \"enabled\": true, \"data_retention\": \"7d\" } },"
<> " { \"name\": \"my-data-stream-2\", \"lifecycle\": { \"enabled\": true, \"data_retention\": \"7d\" } } ] }"
Just decoded = decode docsExample :: Maybe GetDataStreamLifecyclesResponse
length (dataStreamLifecycles decoded) `shouldBe` 2
let names = [(\(DataStreamName n) -> n) (dataStreamLifecycleInfoName i) | i <- dataStreamLifecycles decoded]
names `shouldBe` ["my-data-stream-1", "my-data-stream-2"]
let firstStream = head (dataStreamLifecycles decoded)
Just firstLifecycle = dataStreamLifecycleInfoLifecycle firstStream
dataStreamLifecycleEnabled firstLifecycle `shouldBe` Just True
dataStreamLifecycleDataRetention firstLifecycle `shouldBe` Just "7d"
-- ============================================================
-- Modify data stream (POST /_data_stream/_modify, ES 7.16+)
-- ============================================================
describe "ModifyDataStreamAction JSON" $ do
it "decodes an add_backing_index action" $ do
let decoded = decode "{ \"add_backing_index\": { \"data_stream\": \"logs\", \"index\": \".ds-logs-000001\" } }" :: Maybe ModifyDataStreamAction
case decoded of
Just (AddBackingIndex (DataStreamName "logs") _) -> pure ()
other -> expectationFailure ("expected AddBackingIndex, got " <> show other)
it "decodes a remove_backing_index action" $ do
let decoded = decode "{ \"remove_backing_index\": { \"data_stream\": \"logs\", \"index\": \".ds-logs-000001\" } }" :: Maybe ModifyDataStreamAction
case decoded of
Just (RemoveBackingIndex (DataStreamName "logs") _) -> pure ()
other -> expectationFailure ("expected RemoveBackingIndex, got " <> show other)
it "round-trips add_backing_index through ToJSON/FromJSON" $ do
let value = AddBackingIndex (DataStreamName "s") [qqIndexName|i|]
(decode . encode) value `shouldBe` Just value
it "round-trips remove_backing_index through ToJSON/FromJSON" $ do
let value = RemoveBackingIndex (DataStreamName "s") [qqIndexName|i|]
(decode . encode) value `shouldBe` Just value
it "encodes add_backing_index as a single-key object with nested target" $ do
let value = AddBackingIndex (DataStreamName "s") [qqIndexName|i|]
encode value `shouldBe` "{\"add_backing_index\":{\"data_stream\":\"s\",\"index\":\"i\"}}"
it "encodes remove_backing_index as a single-key object with nested target" $ do
let value = RemoveBackingIndex (DataStreamName "s") [qqIndexName|i|]
encode value `shouldBe` "{\"remove_backing_index\":{\"data_stream\":\"s\",\"index\":\"i\"}}"
it "decodes the verbatim ES docs add_backing_index target (dot-prefixed index name)" $ do
-- From the official curl example at
-- https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-modify-data-stream
let decoded = decode "{ \"add_backing_index\": { \"data_stream\": \"my-data-stream\", \"index\": \".ds-my-data-stream-2023.07.26-000001-downsample\" } }" :: Maybe ModifyDataStreamAction
case decoded of
Just (AddBackingIndex (DataStreamName "my-data-stream") ix)
| unIndexName ix == ".ds-my-data-stream-2023.07.26-000001-downsample" -> pure ()
other -> expectationFailure ("verbatim add_backing_index did not decode, got " <> show other)
it "rejects an action with no discriminator key" $ do
let decoded = decode "{ \"data_stream\": \"s\", \"index\": \"i\" }" :: Maybe ModifyDataStreamAction
decoded `shouldBe` Nothing
it "rejects a non-object action" $ do
let decoded = decode "[]" :: Maybe ModifyDataStreamAction
decoded `shouldBe` Nothing
it "rejects an action whose target is missing 'index'" $ do
let decoded = decode "{ \"add_backing_index\": { \"data_stream\": \"s\" } }" :: Maybe ModifyDataStreamAction
decoded `shouldBe` Nothing
it "rejects an action carrying both discriminator keys (server-side they are mutually exclusive)" $ do
let both =
decode
"{ \"add_backing_index\": { \"data_stream\": \"a\", \"index\": \"x\" }, \"remove_backing_index\": { \"data_stream\": \"r\", \"index\": \"y\" } }" ::
Maybe ModifyDataStreamAction
both `shouldBe` Nothing
describe "ModifyDataStreamRequest JSON" $ do
it "decodes a request with multiple actions" $ do
let payload =
"{ \"actions\": ["
<> " { \"remove_backing_index\": { \"data_stream\": \"my-data-stream\", \"index\": \".ds-my-data-stream-2023.07.26-000001\" } },"
<> " { \"add_backing_index\": { \"data_stream\": \"my-data-stream\", \"index\": \".ds-my-data-stream-2023.07.26-000001-downsample\" } } ] }"
decoded = decode payload :: Maybe ModifyDataStreamRequest
length . modifyDataStreamActions <$> decoded `shouldBe` Just 2
it "decodes an empty actions array as the empty list" $ do
let decoded = decode "{ \"actions\": [] }" :: Maybe ModifyDataStreamRequest
(modifyDataStreamActions <$> decoded) `shouldBe` Just []
it "round-trips through ToJSON/FromJSON" $ do
let value = ModifyDataStreamRequest [AddBackingIndex (DataStreamName "s") [qqIndexName|i|]]
(decode . encode) value `shouldBe` Just value
it "encodes the two-action example verbatim from the ES docs curl" $ do
-- The IndexName constructor is not exported, so build the request via
-- 'decode' of the verbatim docs payload (which goes through
-- mkIndexNameSystem and accepts the dot-prefixed backing-index names),
-- then assert that re-encoding reproduces the exact byte-for-byte body.
let payload =
"{ \"actions\": ["
<> " { \"remove_backing_index\": { \"data_stream\": \"my-data-stream\", \"index\": \".ds-my-data-stream-2023.07.26-000001\" } },"
<> " { \"add_backing_index\": { \"data_stream\": \"my-data-stream\", \"index\": \".ds-my-data-stream-2023.07.26-000001-downsample\" } } ] }"
Just value = decode payload :: Maybe ModifyDataStreamRequest
encode value
`shouldBe` "{\"actions\":[{\"remove_backing_index\":{\"data_stream\":\"my-data-stream\",\"index\":\".ds-my-data-stream-2023.07.26-000001\"}},{\"add_backing_index\":{\"data_stream\":\"my-data-stream\",\"index\":\".ds-my-data-stream-2023.07.26-000001-downsample\"}}]}"
it "rejects a request missing the actions field" $ do
let decoded = decode "{}" :: Maybe ModifyDataStreamRequest
decoded `shouldBe` Nothing
it "rejects a top-level array" $ do
let decoded = decode "[]" :: Maybe ModifyDataStreamRequest
decoded `shouldBe` Nothing
describe "modifyDataStream endpoint shape" $ do
-- Pure checks against the BHRequest shape; no live backend needed.
it "POSTs /_data_stream/_modify with no stream name in the path" $ do
let req = ES7.modifyDataStream (ModifyDataStreamRequest [AddBackingIndex (DataStreamName "logs") [qqIndexName|logs-000001|]])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "_modify"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "uses the POST method" $ do
let req = ES7.modifyDataStream (ModifyDataStreamRequest [RemoveBackingIndex (DataStreamName "logs") [qqIndexName|logs-000001|]])
bhRequestMethod req `shouldBe` "POST"
it "sends the encoded ModifyDataStreamRequest as the body" $ do
let req = ES7.modifyDataStream (ModifyDataStreamRequest [AddBackingIndex (DataStreamName "logs") [qqIndexName|logs-000001|]])
bhRequestBody req `shouldBe` Just "{\"actions\":[{\"add_backing_index\":{\"data_stream\":\"logs\",\"index\":\"logs-000001\"}}]}"
it "sends a multi-action body when given multiple actions" $ do
let req =
ES7.modifyDataStream
( ModifyDataStreamRequest
[ RemoveBackingIndex (DataStreamName "a") [qqIndexName|ds-a-000001|],
AddBackingIndex (DataStreamName "b") [qqIndexName|ds-b-000001|]
]
)
bhRequestBody req
`shouldBe` Just
"{\"actions\":[{\"remove_backing_index\":{\"data_stream\":\"a\",\"index\":\"ds-a-000001\"}},{\"add_backing_index\":{\"data_stream\":\"b\",\"index\":\"ds-b-000001\"}}]}"
-- ============================================================
-- dataStreamExists (HEAD /_data_stream/{name})
-- ============================================================
describe "dataStreamExists endpoint shape" $ do
-- Pure checks against the BHRequest shape; no live backend needed.
it "HEADs /_data_stream/{name} for the given data stream" $ do
let req = ES7.dataStreamExists (DataStreamName "logs-foobar")
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "logs-foobar"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "uses the HEAD method" $ do
let req = ES7.dataStreamExists (DataStreamName "logs-foobar")
bhRequestMethod req `shouldBe` "HEAD"
it "does not attach a body (HEAD semantics)" $ do
let req = ES7.dataStreamExists (DataStreamName "logs-foobar")
bhRequestBody req `shouldBe` Nothing
-- ============================================================
-- PutDataStreamsLifecycleRequest (bulk PUT /_data_stream/_lifecycle)
-- ============================================================
describe "PutDataStreamsLifecycleRequest JSON" $ do
it "decodes a request with two lifecycle entries" $ do
let payload =
"{ \"data_streams\": ["
<> " { \"name\": \"a\", \"lifecycle\": { \"data_retention\": \"1d\" } },"
<> " { \"name\": \"b\", \"lifecycle\": { \"data_retention\": \"2d\" } } ] }"
Just decoded = decode payload :: Maybe PutDataStreamsLifecycleRequest
length (putDataStreamsLifecycleRequestStreams decoded) `shouldBe` 2
it "decodes an empty data_streams array" $ do
let decoded = decode "{ \"data_streams\": [] }" :: Maybe PutDataStreamsLifecycleRequest
(putDataStreamsLifecycleRequestStreams <$> decoded) `shouldBe` Just []
it "round-trips through ToJSON/FromJSON" $ do
let payload =
"{ \"data_streams\": ["
<> " { \"name\": \"a\", \"lifecycle\": { \"data_retention\": \"1d\" } } ] }"
Just decoded = decode payload :: Maybe PutDataStreamsLifecycleRequest
(decode . encode) decoded `shouldBe` Just decoded
it "round-trips an entry whose lifecycle is Nothing (disables management)" $ do
let value =
PutDataStreamsLifecycleRequest
[DataStreamLifecycleInfo (DataStreamName "unmanaged") Nothing]
(decode . encode) value `shouldBe` Just value
it "encodes as a {\"data_streams\":[...]} object" $ do
let value =
PutDataStreamsLifecycleRequest
[ DataStreamLifecycleInfo
(DataStreamName "ds")
(Just (DataStreamLifecycle (Just "7d") Nothing Nothing Nothing Nothing Nothing))
]
encode value
`shouldBe` "{\"data_streams\":[{\"lifecycle\":{\"data_retention\":\"7d\"},\"name\":\"ds\"}]}"
it "encodes an empty request as {\"data_streams\":[]}" $ do
encode (PutDataStreamsLifecycleRequest []) `shouldBe` "{\"data_streams\":[]}"
it "rejects a request missing the data_streams field" $ do
let decoded = decode "{}" :: Maybe PutDataStreamsLifecycleRequest
decoded `shouldBe` Nothing
it "rejects a top-level array" $ do
let decoded = decode "[]" :: Maybe PutDataStreamsLifecycleRequest
decoded `shouldBe` Nothing
describe "putDataStreamsLifecycle endpoint shape" $ do
it "PUTs /_data_stream/_lifecycle for the bulk lifecycle update" $ do
let req =
ES8.putDataStreamsLifecycle
( PutDataStreamsLifecycleRequest
[ DataStreamLifecycleInfo
(DataStreamName "logs-foobar")
(Just (DataStreamLifecycle (Just "7d") Nothing Nothing Nothing Nothing Nothing))
]
)
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "_lifecycle"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "uses the PUT method" $ do
let req = ES8.putDataStreamsLifecycle (PutDataStreamsLifecycleRequest [])
bhRequestMethod req `shouldBe` "PUT"
it "sends the encoded PutDataStreamsLifecycleRequest as the body" $ do
let req =
ES8.putDataStreamsLifecycle
( PutDataStreamsLifecycleRequest
[ DataStreamLifecycleInfo
(DataStreamName "ds")
(Just (DataStreamLifecycle (Just "7d") Nothing Nothing Nothing Nothing Nothing))
]
)
bhRequestBody req `shouldBe` Just "{\"data_streams\":[{\"lifecycle\":{\"data_retention\":\"7d\"},\"name\":\"ds\"}]}"
it "sends an empty data_streams array body when given no entries" $ do
let req = ES8.putDataStreamsLifecycle (PutDataStreamsLifecycleRequest [])
bhRequestBody req `shouldBe` Just "{\"data_streams\":[]}"
-- ============================================================
-- Data Stream Options (GET/PUT/DELETE /_data_stream/{name}/_options, ES 8.19+)
-- ============================================================
describe "DataStreamFailureStoreLifecycle JSON" $ do
it "decodes a fully-populated lifecycle" $ do
let decoded = decode "{ \"data_retention\": \"7d\", \"enabled\": true }" :: Maybe DataStreamFailureStoreLifecycle
decoded `shouldBe` Just (DataStreamFailureStoreLifecycle (Just "7d") (Just True))
it "decodes an empty object as all-Nothing" $ do
let decoded = decode "{}" :: Maybe DataStreamFailureStoreLifecycle
decoded `shouldBe` Just (DataStreamFailureStoreLifecycle Nothing Nothing)
it "round-trips through ToJSON/FromJSON" $ do
let value = DataStreamFailureStoreLifecycle (Just "30d") (Just False)
(decode . encode) value `shouldBe` Just value
it "encodes {} for the all-Nothing value (omitNulls)" $ do
encode (DataStreamFailureStoreLifecycle Nothing Nothing) `shouldBe` "{}"
it "rejects a top-level array" $ do
let decoded = decode "[]" :: Maybe DataStreamFailureStoreLifecycle
decoded `shouldBe` Nothing
describe "DataStreamFailureStore JSON" $ do
it "decodes the verbatim ES docs example (enabled + lifecycle)" $ do
let decoded = decode "{ \"enabled\": true, \"lifecycle\": { \"data_retention\": \"string\", \"enabled\": true } }" :: Maybe DataStreamFailureStore
decoded
`shouldBe` Just
( DataStreamFailureStore
(Just True)
(Just (DataStreamFailureStoreLifecycle (Just "string") (Just True)))
)
it "decodes a store with only `enabled` set" $ do
let decoded = decode "{ \"enabled\": false }" :: Maybe DataStreamFailureStore
decoded `shouldBe` Just (DataStreamFailureStore (Just False) Nothing)
it "decodes an empty object as all-Nothing" $ do
let decoded = decode "{}" :: Maybe DataStreamFailureStore
decoded `shouldBe` Just (DataStreamFailureStore Nothing Nothing)
it "round-trips through ToJSON/FromJSON" $ do
let value =
DataStreamFailureStore
(Just True)
(Just (DataStreamFailureStoreLifecycle (Just "7d") Nothing))
(decode . encode) value `shouldBe` Just value
it "encodes the verbatim ES docs curl --data body" $ do
let value =
DataStreamFailureStore
(Just True)
(Just (DataStreamFailureStoreLifecycle (Just "string") (Just True)))
encode value `shouldBe` "{\"enabled\":true,\"lifecycle\":{\"data_retention\":\"string\",\"enabled\":true}}"
it "encodes {} for the all-Nothing value (omitNulls)" $ do
encode (DataStreamFailureStore Nothing Nothing) `shouldBe` "{}"
it "rejects a top-level array" $ do
let decoded = decode "[]" :: Maybe DataStreamFailureStore
decoded `shouldBe` Nothing
describe "UpdateDataStreamOptionsRequest JSON" $ do
it "decodes a request with failure_store populated" $ do
let decoded = decode "{ \"failure_store\": { \"enabled\": true } }" :: Maybe UpdateDataStreamOptionsRequest
decoded
`shouldBe` Just
(UpdateDataStreamOptionsRequest (Just (DataStreamFailureStore (Just True) Nothing)))
it "decodes a request with no failure_store key as Nothing" $ do
let decoded = decode "{}" :: Maybe UpdateDataStreamOptionsRequest
decoded `shouldBe` Just (UpdateDataStreamOptionsRequest Nothing)
it "decodes a request with explicit null failure_store as Nothing" $ do
let decoded = decode "{ \"failure_store\": null }" :: Maybe UpdateDataStreamOptionsRequest
decoded `shouldBe` Just (UpdateDataStreamOptionsRequest Nothing)
it "round-trips through ToJSON/FromJSON" $ do
let value =
UpdateDataStreamOptionsRequest
(Just (DataStreamFailureStore (Just False) Nothing))
(decode . encode) value `shouldBe` Just value
it "round-trips a Nothing failure_store through ToJSON/FromJSON" $ do
let value = UpdateDataStreamOptionsRequest Nothing
(decode . encode) value `shouldBe` Just value
it "encodes the verbatim ES docs curl body (failure_store wrapping the docs example)" $ do
let value =
UpdateDataStreamOptionsRequest
( Just
( DataStreamFailureStore
(Just True)
(Just (DataStreamFailureStoreLifecycle (Just "string") (Just True)))
)
)
encode value `shouldBe` "{\"failure_store\":{\"enabled\":true,\"lifecycle\":{\"data_retention\":\"string\",\"enabled\":true}}}"
it "encodes a Nothing failure_store as {} (omitNulls)" $ do
encode (UpdateDataStreamOptionsRequest Nothing) `shouldBe` "{}"
it "rejects a top-level array" $ do
let decoded = decode "[]" :: Maybe UpdateDataStreamOptionsRequest
decoded `shouldBe` Nothing
describe "DataStreamOptions JSON" $ do
it "decodes an options object with failure_store" $ do
let decoded = decode "{ \"failure_store\": { \"enabled\": false } }" :: Maybe DataStreamOptions
decoded `shouldBe` Just (DataStreamOptions (Just (DataStreamFailureStore (Just False) Nothing)))
it "decodes an empty object as all-Nothing" $ do
let decoded = decode "{}" :: Maybe DataStreamOptions
decoded `shouldBe` Just (DataStreamOptions Nothing)
it "round-trips through ToJSON/FromJSON" $ do
let value = DataStreamOptions (Just (DataStreamFailureStore (Just True) Nothing))
(decode . encode) value `shouldBe` Just value
it "decodes an options object with unknown future keys (forward-compat)" $ do
-- The `options` object is open-ended on the server side; unknown keys
-- must decode without breaking the client.
let decoded = decode "{ \"failure_store\": { \"enabled\": true }, \"future_field\": 42 }" :: Maybe DataStreamOptions
decoded `shouldBe` Just (DataStreamOptions (Just (DataStreamFailureStore (Just True) Nothing)))
describe "DataStreamOptionsEntry JSON" $ do
it "decodes an entry with options" $ do
let decoded = decode "{ \"name\": \"logs\", \"options\": { \"failure_store\": { \"enabled\": true } } }" :: Maybe DataStreamOptionsEntry
decoded
`shouldBe` Just
( DataStreamOptionsEntry
(DataStreamName "logs")
(Just (DataStreamOptions (Just (DataStreamFailureStore (Just True) Nothing))))
)
it "decodes an entry whose options key is absent (unmanaged stream)" $ do
let decoded = decode "{ \"name\": \"plain\" }" :: Maybe DataStreamOptionsEntry
decoded `shouldBe` Just (DataStreamOptionsEntry (DataStreamName "plain") Nothing)
it "decodes an entry whose options key is explicitly null as Nothing" $ do
let decoded = decode "{ \"name\": \"plain\", \"options\": null }" :: Maybe DataStreamOptionsEntry
decoded `shouldBe` Just (DataStreamOptionsEntry (DataStreamName "plain") Nothing)
it "round-trips an entry with options through ToJSON/FromJSON" $ do
let value =
DataStreamOptionsEntry
(DataStreamName "ds")
(Just (DataStreamOptions (Just (DataStreamFailureStore (Just True) Nothing))))
(decode . encode) value `shouldBe` Just value
it "round-trips an entry with no options through ToJSON/FromJSON" $ do
let value = DataStreamOptionsEntry (DataStreamName "ds") Nothing
(decode . encode) value `shouldBe` Just value
it "rejects an entry missing the name field" $ do
let decoded = decode "{ \"options\": {} }" :: Maybe DataStreamOptionsEntry
decoded `shouldBe` Nothing
it "rejects a top-level array" $ do
let decoded = decode "[]" :: Maybe DataStreamOptionsEntry
decoded `shouldBe` Nothing
describe "GetDataStreamOptionsResponse JSON" $ do
it "decodes a response with multiple entries" $ do
let payload =
"{ \"data_streams\": ["
<> " { \"name\": \"a\", \"options\": { \"failure_store\": { \"enabled\": true } } },"
<> " { \"name\": \"b\" } ] }"
decoded = decode payload :: Maybe GetDataStreamOptionsResponse
length . getDataStreamOptionsResponseDataStreams <$> decoded `shouldBe` Just 2
it "decodes an empty data_streams array" $ do
let decoded = decode "{ \"data_streams\": [] }" :: Maybe GetDataStreamOptionsResponse
(getDataStreamOptionsResponseDataStreams <$> decoded) `shouldBe` Just []
it "round-trips through ToJSON/FromJSON" $ do
let value =
GetDataStreamOptionsResponse
[ DataStreamOptionsEntry
(DataStreamName "ds")
(Just (DataStreamOptions (Just (DataStreamFailureStore (Just True) Nothing))))
]
(decode . encode) value `shouldBe` Just value
it "rejects a response missing the data_streams field" $ do
let decoded = decode "{}" :: Maybe GetDataStreamOptionsResponse
decoded `shouldBe` Nothing
it "rejects a top-level array" $ do
let decoded = decode "[]" :: Maybe GetDataStreamOptionsResponse
decoded `shouldBe` Nothing
describe "getDataStreamOptions endpoint shape" $ do
-- Pure checks against the BHRequest shape; no live backend needed.
it "GETs /_data_stream/_options when given Nothing" $ do
let req = ES8.getDataStreamOptions Nothing
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "_options"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "GETs /_data_stream/_options when given Just []" $ do
let req = ES8.getDataStreamOptions (Just [])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "_options"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "GETs /_data_stream/{a,b,c}/_options for multiple names" $ do
let req = ES8.getDataStreamOptions (Just [DataStreamName "a", DataStreamName "b", DataStreamName "c"])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "a,b,c", "_options"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "GETs /_data_stream/{name}/_options for a single name" $ do
let req = ES8.getDataStreamOptions (Just [DataStreamName "logs"])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "logs", "_options"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "uses the GET method" $ do
let req = ES8.getDataStreamOptions (Just [DataStreamName "logs"])
bhRequestMethod req `shouldBe` "GET"
it "does not attach a body (GET semantics)" $ do
let req = ES8.getDataStreamOptions Nothing
bhRequestBody req `shouldBe` Nothing
describe "updateDataStreamOptions endpoint shape" $ do
it "PUTs /_data_stream/{name}/_options for a single name" $ do
let req = ES8.updateDataStreamOptions (Just [DataStreamName "logs"]) (UpdateDataStreamOptionsRequest Nothing)
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "logs", "_options"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "PUTs /_data_stream/{a,b}/_options for multiple names" $ do
let req = ES8.updateDataStreamOptions (Just [DataStreamName "a", DataStreamName "b"]) (UpdateDataStreamOptionsRequest Nothing)
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "a,b", "_options"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "PUTs /_data_stream/*/_options when given Nothing (server resolves wildcard)" $ do
let req = ES8.updateDataStreamOptions Nothing (UpdateDataStreamOptionsRequest Nothing)
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "*", "_options"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "PUTs /_data_stream/*/_options when given Just [] (server resolves wildcard)" $ do
let req = ES8.updateDataStreamOptions (Just []) (UpdateDataStreamOptionsRequest Nothing)
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "*", "_options"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "uses the PUT method" $ do
let req = ES8.updateDataStreamOptions (Just [DataStreamName "logs"]) (UpdateDataStreamOptionsRequest Nothing)
bhRequestMethod req `shouldBe` "PUT"
it "sends the encoded UpdateDataStreamOptionsRequest as the body" $ do
let req =
ES8.updateDataStreamOptions
(Just [DataStreamName "logs"])
( UpdateDataStreamOptionsRequest
(Just (DataStreamFailureStore (Just True) Nothing))
)
bhRequestBody req `shouldBe` Just "{\"failure_store\":{\"enabled\":true}}"
it "sends {} as the body when failure_store is Nothing (omitNulls)" $ do
let req = ES8.updateDataStreamOptions (Just [DataStreamName "logs"]) (UpdateDataStreamOptionsRequest Nothing)
bhRequestBody req `shouldBe` Just "{}"
describe "deleteDataStreamOptions endpoint shape" $ do
it "DELETEs /_data_stream/{name}/_options for a single name" $ do
let req = ES8.deleteDataStreamOptions (Just [DataStreamName "logs"])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "logs", "_options"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "DELETEs /_data_stream/{a,b}/_options for multiple names" $ do
let req = ES8.deleteDataStreamOptions (Just [DataStreamName "a", DataStreamName "b"])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "a,b", "_options"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "DELETEs /_data_stream/*/_options when given Nothing (server resolves wildcard)" $ do
let req = ES8.deleteDataStreamOptions Nothing
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "*", "_options"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "DELETEs /_data_stream/*/_options when given Just [] (server resolves wildcard)" $ do
let req = ES8.deleteDataStreamOptions (Just [])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "*", "_options"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "uses the DELETE method" $ do
let req = ES8.deleteDataStreamOptions (Just [DataStreamName "logs"])
bhRequestMethod req `shouldBe` "DELETE"
it "does not attach a body (DELETE semantics)" $ do
let req = ES8.deleteDataStreamOptions (Just [DataStreamName "logs"])
bhRequestBody req `shouldBe` Nothing
-- =====================================================================
-- GET /_lifecycle/stats (indices-get-data-lifecycle-stats)
-- =====================================================================
describe "getDataStreamLifecycleStats JSON" $ do
it "decodes the verbatim ES docs example with data_streams_count (plural)" $ do
let payload =
"{ \"last_run_duration_in_millis\": 2"
<> ", \"last_run_duration\": \"2ms\""
<> ", \"time_between_starts_in_millis\": 9998"
<> ", \"time_between_starts\": \"9.99s\""
<> ", \"data_streams_count\": 2"
<> ", \"data_streams\": ["
<> " { \"name\": \"my-data-stream\", \"backing_indices_in_total\": 2, \"backing_indices_in_error\": 0 },"
<> " { \"name\": \"my-other-stream\", \"backing_indices_in_total\": 2, \"backing_indices_in_error\": 1 } ] }"
Just decoded = decode payload :: Maybe DataStreamLifecycleStats
dataStreamLifecycleStatsCount decoded `shouldBe` 2
dataStreamLifecycleStatsLastRunDurationInMillis decoded `shouldBe` Just 2
dataStreamLifecycleStatsLastRunDuration decoded `shouldBe` Just "2ms"
dataStreamLifecycleStatsTimeBetweenStartsInMillis decoded `shouldBe` Just 9998
dataStreamLifecycleStatsTimeBetweenStarts decoded `shouldBe` Just "9.99s"
length (dataStreamLifecycleStatsDataStreams decoded) `shouldBe` 2
it "also accepts data_stream_count (singular schema form)" $ do
let Just decoded = decode "{ \"data_stream_count\": 0, \"data_streams\": [] }" :: Maybe DataStreamLifecycleStats
dataStreamLifecycleStatsCount decoded `shouldBe` 0
it "rejects a payload missing both count keys" $ do
let decoded = decode "{ \"data_streams\": [] }" :: Maybe DataStreamLifecycleStats
decoded `shouldBe` Nothing
it "decodes a per-stream entry" $ do
let Just decoded = decode "{ \"name\": \"ds\", \"backing_indices_in_total\": 5, \"backing_indices_in_error\": 2 }" :: Maybe DataStreamLifecycleStatsEntry
dataStreamLifecycleStatsEntryName decoded `shouldBe` DataStreamName "ds"
dataStreamLifecycleStatsEntryBackingIndicesInTotal decoded `shouldBe` 5
dataStreamLifecycleStatsEntryBackingIndicesInError decoded `shouldBe` 2
it "round-trips a fully-populated stats response" $ do
let value =
DataStreamLifecycleStats
{ dataStreamLifecycleStatsLastRunDurationInMillis = Just 2,
dataStreamLifecycleStatsLastRunDuration = Just "2ms",
dataStreamLifecycleStatsTimeBetweenStartsInMillis = Just 9998,
dataStreamLifecycleStatsTimeBetweenStarts = Just "9.99s",
dataStreamLifecycleStatsCount = 1,
dataStreamLifecycleStatsDataStreams =
[ DataStreamLifecycleStatsEntry (DataStreamName "ds") 2 0
]
}
(decode . encode) value `shouldBe` Just value
it "encodes count as data_streams_count (plural, matching the docs example)" $ do
let value =
DataStreamLifecycleStats
{ dataStreamLifecycleStatsLastRunDurationInMillis = Nothing,
dataStreamLifecycleStatsLastRunDuration = Nothing,
dataStreamLifecycleStatsTimeBetweenStartsInMillis = Nothing,
dataStreamLifecycleStatsTimeBetweenStarts = Nothing,
dataStreamLifecycleStatsCount = 0,
dataStreamLifecycleStatsDataStreams = []
}
-- omitNulls drops the empty data_streams array (documented
-- behaviour — see "S6: omitNulls collapse of empty downsampling
-- list"); the count survives because 0 is not null.
encode value `shouldBe` "{\"data_streams_count\":0}"
it "round-trips the empty case (omitNulls drops data_streams, FromJSON tolerates its absence)" $ do
let value =
DataStreamLifecycleStats
{ dataStreamLifecycleStatsLastRunDurationInMillis = Nothing,
dataStreamLifecycleStatsLastRunDuration = Nothing,
dataStreamLifecycleStatsTimeBetweenStartsInMillis = Nothing,
dataStreamLifecycleStatsTimeBetweenStarts = Nothing,
dataStreamLifecycleStatsCount = 0,
dataStreamLifecycleStatsDataStreams = []
}
-- encode drops data_streams, decode tolerates its absence (.!= [])
(decode . encode) value `shouldBe` Just value
describe "getDataStreamLifecycleStats endpoint shape (ES8)" $ do
it "GETs /_lifecycle/stats" $ do
let req = ES8.getDataStreamLifecycleStats
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_lifecycle", "stats"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "uses the GET method" $ do
let req = ES8.getDataStreamLifecycleStats
bhRequestMethod req `shouldBe` "GET"
it "does not attach a body" $ do
let req = ES8.getDataStreamLifecycleStats
bhRequestBody req `shouldBe` Nothing
describe "getDataStreamLifecycleStats endpoint shape (ES9 delegates to ES8)" $ do
it "GETs /_lifecycle/stats" $ do
let req = ES9.getDataStreamLifecycleStats
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_lifecycle", "stats"]
-- =====================================================================
-- GET /{index}/_lifecycle/explain (indices-explain-data-lifecycle)
-- =====================================================================
describe "explainDataStreamLifecycle JSON" $ do
it "decodes the verbatim ES docs example (single managed backing index)" $ do
let payload =
"{ \"indices\": {"
<> " \".ds-metrics-2023.03.22-000001\": {"
<> " \"index\": \".ds-metrics-2023.03.22-000001\","
<> " \"managed_by_lifecycle\": true,"
<> " \"index_creation_date_millis\": 1679475563571,"
<> " \"time_since_index_creation\": \"843ms\","
<> " \"rollover_date_millis\": 1679475564293,"
<> " \"time_since_rollover\": \"121ms\","
<> " \"lifecycle\": {},"
<> " \"generation_time\": \"121ms\" } } }"
Just decoded = decode payload :: Maybe ExplainDataStreamLifecycleResponse
M.size (explainDataStreamLifecycleResponseIndices decoded) `shouldBe` 1
it "decodes an entry with an error string" $ do
let payload =
"{ \"indices\": {"
<> " \".ds-broken-000001\": {"
<> " \"index\": \".ds-broken-000001\","
<> " \"managed_by_lifecycle\": true,"
<> " \"error\": \"something went wrong\" } } }"
Just decoded = decode payload :: Maybe ExplainDataStreamLifecycleResponse
let entry = (M.elems . explainDataStreamLifecycleResponseIndices) decoded
length entry `shouldBe` 1
explainDataStreamLifecycleIndexError (head entry) `shouldBe` Just "something went wrong"
explainDataStreamLifecycleIndexManagedByLifecycle (head entry) `shouldBe` True
it "round-trips a fully-populated entry" $ do
let value =
ExplainDataStreamLifecycleIndex
{ explainDataStreamLifecycleIndexIndex = mkIdx ".ds-foo-000001",
explainDataStreamLifecycleIndexManagedByLifecycle = True,
explainDataStreamLifecycleIndexCreationDateMillis = Just 1679475563571,
explainDataStreamLifecycleIndexTimeSinceIndexCreation = Just "843ms",
explainDataStreamLifecycleIndexRolloverDateMillis = Just 1679475564293,
explainDataStreamLifecycleIndexTimeSinceRollover = Just "121ms",
explainDataStreamLifecycleIndexLifecycle = Nothing,
explainDataStreamLifecycleIndexGenerationTime = Just "121ms",
explainDataStreamLifecycleIndexError = Nothing
}
(decode . encode) value `shouldBe` Just value
it "round-trips the top-level response with a Map" $ do
let key = mkIdx ".ds-foo-000001"
value = ExplainDataStreamLifecycleResponse (M.singleton key (minimalExplainEntry key))
(decode . encode) value `shouldBe` Just value
it "rejects a response missing the indices field" $ do
let decoded = decode "{}" :: Maybe ExplainDataStreamLifecycleResponse
decoded `shouldBe` Nothing
describe "explainDataStreamLifecycle endpoint shape (ES8)" $ do
it "GETs /_lifecycle/explain when given Nothing" $ do
let req = ES8.explainDataStreamLifecycle Nothing
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_lifecycle", "explain"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "GETs /_lifecycle/explain when given Just []" $ do
let req = ES8.explainDataStreamLifecycle (Just [])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_lifecycle", "explain"]
it "GETs /{index}/_lifecycle/explain for a single index" $ do
let req = ES8.explainDataStreamLifecycle (Just [mkIdx ".ds-foo-000001"])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` [".ds-foo-000001", "_lifecycle", "explain"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "GETs /{a,b}/_lifecycle/explain for multiple indices (comma-joined)" $ do
let req = ES8.explainDataStreamLifecycle (Just [mkIdx ".ds-a-000001", mkIdx ".ds-b-000001"])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` [".ds-a-000001,.ds-b-000001", "_lifecycle", "explain"]
it "does not attach a body" $ do
let req = ES8.explainDataStreamLifecycle Nothing
bhRequestBody req `shouldBe` Nothing
it "emits include_defaults=true when set" $ do
let opts = defaultExplainDataStreamLifecycleOptions {explainDataStreamLifecycleOptionsIncludeDefaults = Just True}
req = ES8.explainDataStreamLifecycleWith (Just [mkIdx ".ds-foo-000001"]) opts
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` [("include_defaults", Just "true")]
it "emits master_timeout when set" $ do
let opts = defaultExplainDataStreamLifecycleOptions {explainDataStreamLifecycleOptionsMasterTimeout = Just "30s"}
req = ES8.explainDataStreamLifecycleWith Nothing opts
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` [("master_timeout", Just "30s")]
it "emits both params when both are set" $ do
let opts =
defaultExplainDataStreamLifecycleOptions
{ explainDataStreamLifecycleOptionsIncludeDefaults = Just False,
explainDataStreamLifecycleOptionsMasterTimeout = Just "1m"
}
req = ES8.explainDataStreamLifecycleWith Nothing opts
sort (getRawEndpointQueries (bhRequestEndpoint req))
`shouldBe` sort [("include_defaults", Just "false"), ("master_timeout", Just "1m")]
describe "explainDataStreamLifecycle endpoint shape (ES9 delegates to ES8)" $ do
it "GETs /_lifecycle/explain when given Nothing" $ do
let req = ES9.explainDataStreamLifecycle Nothing
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_lifecycle", "explain"]
it "GETs /{index}/_lifecycle/explain for a single index" $ do
let req = ES9.explainDataStreamLifecycle (Just [mkIdx ".ds-foo-000001"])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` [".ds-foo-000001", "_lifecycle", "explain"]
-- =====================================================================
-- GET/PUT /_data_stream/{name}/_mappings
-- (indices-get/put-data-stream-mappings) — ES9 only
-- =====================================================================
describe "getDataStreamMappings JSON" $ do
it "decodes the verbatim ES docs example" $ do
let payload =
"{ \"data_streams\": ["
<> " { \"name\": \"my-data-stream\","
<> " \"mappings\": { \"properties\": { \"field1\": { \"type\": \"ip\" } } },"
<> " \"effective_mappings\": { \"properties\": { \"field1\": { \"type\": \"ip\" }, \"field2\": { \"type\": \"text\" } } } } ] }"
Just decoded = decode payload :: Maybe GetDataStreamMappingsResponse
length (getDataStreamMappingsResponseDataStreams decoded) `shouldBe` 1
let entry = head (getDataStreamMappingsResponseDataStreams decoded)
getDataStreamMappingsEntryName entry `shouldBe` DataStreamName "my-data-stream"
it "round-trips a fully-populated response" $ do
let value =
GetDataStreamMappingsResponse
[ GetDataStreamMappingsEntry
(DataStreamName "ds")
(object ["properties" .= object ["foo" .= object ["type" .= ("text" :: Text)]]])
(object ["properties" .= object ["foo" .= object ["type" .= ("text" :: Text)]]])
]
(decode . encode) value `shouldBe` Just value
it "rejects a response missing data_streams" $ do
let decoded = decode "{}" :: Maybe GetDataStreamMappingsResponse
decoded `shouldBe` Nothing
describe "updateDataStreamMappings JSON" $ do
it "decodes a successful PUT response (applied_to_data_stream=true)" $ do
let payload =
"{ \"data_streams\": ["
<> " { \"name\": \"ds\","
<> " \"applied_to_data_stream\": true,"
<> " \"mappings\": { \"properties\": {} },"
<> " \"effective_mappings\": { \"properties\": {} } } ] }"
Just decoded = decode payload :: Maybe UpdateDataStreamMappingsResponse
let entry = head (updateDataStreamMappingsResponseDataStreams decoded)
updateDataStreamMappingsEntryAppliedToDataStream entry `shouldBe` True
updateDataStreamMappingsEntryError entry `shouldBe` Nothing
it "decodes a failed PUT response (applied_to_data_stream=false) with error" $ do
let payload =
"{ \"data_streams\": ["
<> " { \"name\": \"ds\","
<> " \"applied_to_data_stream\": false,"
<> " \"error\": \"Cannot set setting X\" } ] }"
Just decoded = decode payload :: Maybe UpdateDataStreamMappingsResponse
let entry = head (updateDataStreamMappingsResponseDataStreams decoded)
updateDataStreamMappingsEntryAppliedToDataStream entry `shouldBe` False
updateDataStreamMappingsEntryError entry `shouldBe` Just "Cannot set setting X"
it "tolerates missing mappings/effective_mappings (decode as empty objects)" $ do
let payload =
"{ \"data_streams\": ["
<> " { \"name\": \"ds\", \"applied_to_data_stream\": false, \"error\": \"x\" } ] }"
Just decoded = decode payload :: Maybe UpdateDataStreamMappingsResponse
let entry = head (updateDataStreamMappingsResponseDataStreams decoded)
updateDataStreamMappingsEntryMappings entry `shouldBe` object []
describe "getDataStreamMappings endpoint shape (ES9)" $ do
it "GETs /_data_stream/_mappings when given Nothing" $ do
let req = ES9.getDataStreamMappings Nothing
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "_mappings"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "GETs /_data_stream/_mappings when given Just []" $ do
let req = ES9.getDataStreamMappings (Just [])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "_mappings"]
it "GETs /_data_stream/{a,b}/_mappings for multiple names" $ do
let req = ES9.getDataStreamMappings (Just [DataStreamName "a", DataStreamName "b"])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "a,b", "_mappings"]
it "emits master_timeout when set" $ do
let opts = defaultGetDataStreamMappingsOptions {getDataStreamMappingsOptionsMasterTimeout = Just "30s"}
req = ES9.getDataStreamMappingsWith (Just [DataStreamName "ds"]) opts
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` [("master_timeout", Just "30s")]
describe "putDataStreamMappings endpoint shape (ES9)" $ do
it "PUTs /_data_stream/{name}/_mappings for a single name" $ do
let req = ES9.putDataStreamMappings (Just [DataStreamName "ds"]) (object ["properties" .= object []])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "ds", "_mappings"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "PUTs /_data_stream/*/_mappings when given Nothing (wildcard fan-out)" $ do
let req = ES9.putDataStreamMappings Nothing (object ["properties" .= object []])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "*", "_mappings"]
it "PUTs /_data_stream/*/_mappings when given Just [] (wildcard fan-out)" $ do
let req = ES9.putDataStreamMappings (Just []) (object ["properties" .= object []])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "*", "_mappings"]
it "uses the PUT method" $ do
let req = ES9.putDataStreamMappings (Just [DataStreamName "ds"]) (object [])
bhRequestMethod req `shouldBe` "PUT"
it "encodes the mapping object as the body" $ do
let req = ES9.putDataStreamMappings (Just [DataStreamName "ds"]) (object ["properties" .= object ["foo" .= object ["type" .= ("text" :: Text)]]])
bhRequestBody req `shouldBe` Just "{\"properties\":{\"foo\":{\"type\":\"text\"}}}"
it "emits dry_run=true when set" $ do
let opts = defaultUpdateDataStreamMappingsOptions {updateDataStreamMappingsOptionsDryRun = Just True}
req = ES9.putDataStreamMappingsWith (Just [DataStreamName "ds"]) (object []) opts
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` [("dry_run", Just "true")]
-- =====================================================================
-- GET/PUT /_data_stream/{name}/_settings
-- (indices-get/put-data-stream-settings) — ES9 only
-- =====================================================================
describe "getDataStreamSettings JSON" $ do
it "decodes the verbatim ES docs example (numeric-as-string)" $ do
let payload =
"{ \"data_streams\": ["
<> " { \"name\": \"my-data-stream\","
<> " \"settings\": { \"index\": { \"lifecycle\": { \"name\": \"policy\" }, \"number_of_shards\": \"11\" } },"
<> " \"effective_settings\": { \"index\": { \"lifecycle\": { \"name\": \"policy\" }, \"number_of_shards\": \"11\", \"number_of_replicas\": \"0\" } } } ] }"
Just decoded = decode payload :: Maybe GetDataStreamSettingsResponse
length (getDataStreamSettingsResponseDataStreams decoded) `shouldBe` 1
it "round-trips a fully-populated response" $ do
let value =
GetDataStreamSettingsResponse
[ GetDataStreamSettingsEntry
(DataStreamName "ds")
(object ["index" .= object ["number_of_shards" .= ("11" :: Text)]])
(object ["index" .= object ["number_of_shards" .= ("11" :: Text)]])
]
(decode . encode) value `shouldBe` Just value
describe "updateDataStreamSettings JSON" $ do
it "decodes a successful PUT response with index_settings_results" $ do
let payload =
"{ \"data_streams\": ["
<> " { \"name\": \"ds\","
<> " \"applied_to_data_stream\": true,"
<> " \"settings\": { \"index\": { \"number_of_shards\": \"11\" } },"
<> " \"effective_settings\": { \"index\": { \"number_of_shards\": \"11\" } },"
<> " \"index_settings_results\": {"
<> " \"applied_to_data_stream_only\": [\"index.number_of_shards\"],"
<> " \"applied_to_data_stream_and_backing_indices\": [\"index.lifecycle.name\"] } } ] }"
Just decoded = decode payload :: Maybe UpdateDataStreamSettingsResponse
let entry = head (updateDataStreamSettingsResponseDataStreams decoded)
updateDataStreamSettingsEntryAppliedToDataStream entry `shouldBe` True
let Just results = updateDataStreamSettingsEntryIndexSettingsResults entry
indexSettingsResultsAppliedToDataStreamOnly results `shouldBe` ["index.number_of_shards"]
indexSettingsResultsAppliedToDataStreamAndBackingIndices results `shouldBe` ["index.lifecycle.name"]
indexSettingsResultsErrors results `shouldBe` []
it "decodes a partial-failure response with per-index errors" $ do
let payload =
"{ \"data_streams\": ["
<> " { \"name\": \"ds\","
<> " \"applied_to_data_stream\": true,"
<> " \"settings\": {}, \"effective_settings\": {},"
<> " \"index_settings_results\": {"
<> " \"applied_to_data_stream_only\": [],"
<> " \"applied_to_data_stream_and_backing_indices\": [],"
<> " \"errors\": [ { \"index\": \".ds-ds-000001\", \"error\": \"blocked by FORBIDDEN/9\" } ] } } ] }"
Just decoded = decode payload :: Maybe UpdateDataStreamSettingsResponse
let entry = head (updateDataStreamSettingsResponseDataStreams decoded)
Just results = updateDataStreamSettingsEntryIndexSettingsResults entry
length (indexSettingsResultsErrors results) `shouldBe` 1
indexSettingErrorIndex (head (indexSettingsResultsErrors results)) `shouldBe` ".ds-ds-000001"
it "decodes a total-failure response (applied_to_data_stream=false)" $ do
let payload =
"{ \"data_streams\": ["
<> " { \"name\": \"ds\","
<> " \"applied_to_data_stream\": false,"
<> " \"error\": \"Cannot set the following settings\","
<> " \"settings\": {}, \"effective_settings\": {},"
<> " \"index_settings_results\": { \"applied_to_data_stream_only\": [], \"applied_to_data_stream_and_backing_indices\": [] } } ] }"
Just decoded = decode payload :: Maybe UpdateDataStreamSettingsResponse
let entry = head (updateDataStreamSettingsResponseDataStreams decoded)
updateDataStreamSettingsEntryAppliedToDataStream entry `shouldBe` False
updateDataStreamSettingsEntryError entry `shouldBe` Just "Cannot set the following settings"
it "round-trips an IndexSettingError" $ do
let value = IndexSettingError ".ds-ds-000001" "blocked"
(decode . encode) value `shouldBe` Just value
describe "getDataStreamSettings endpoint shape (ES9)" $ do
it "GETs /_data_stream/_settings when given Nothing" $ do
let req = ES9.getDataStreamSettings Nothing
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "_settings"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "GETs /_data_stream/{name}/_settings for a single name" $ do
let req = ES9.getDataStreamSettings (Just [DataStreamName "ds"])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "ds", "_settings"]
it "emits master_timeout when set" $ do
let opts = defaultGetDataStreamSettingsOptions {getDataStreamSettingsOptionsMasterTimeout = Just "1m"}
req = ES9.getDataStreamSettingsWith Nothing opts
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` [("master_timeout", Just "1m")]
describe "putDataStreamSettings endpoint shape (ES9)" $ do
it "PUTs /_data_stream/{name}/_settings for a single name" $ do
let req = ES9.putDataStreamSettings (Just [DataStreamName "ds"]) (object ["index.lifecycle.name" .= ("policy" :: Text)])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "ds", "_settings"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "PUTs /_data_stream/*/_settings when given Nothing (wildcard fan-out)" $ do
let req = ES9.putDataStreamSettings Nothing (object [])
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_data_stream", "*", "_settings"]
it "uses the PUT method" $ do
let req = ES9.putDataStreamSettings (Just [DataStreamName "ds"]) (object [])
bhRequestMethod req `shouldBe` "PUT"
it "encodes the settings object as the body" $ do
let req = ES9.putDataStreamSettings (Just [DataStreamName "ds"]) (object ["index.number_of_shards" .= (11 :: Int)])
bhRequestBody req `shouldBe` Just "{\"index.number_of_shards\":11}"
it "emits dry_run=false when set to False" $ do
let opts = defaultUpdateDataStreamSettingsOptions {updateDataStreamSettingsOptionsDryRun = Just False}
req = ES9.putDataStreamSettingsWith (Just [DataStreamName "ds"]) (object []) opts
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` [("dry_run", Just "false")]
it "emits both dry_run and timeout when set" $ do
let opts =
defaultUpdateDataStreamSettingsOptions
{ updateDataStreamSettingsOptionsDryRun = Just True,
updateDataStreamSettingsOptionsTimeout = Just "5s"
}
req = ES9.putDataStreamSettingsWith Nothing (object []) opts
sort (getRawEndpointQueries (bhRequestEndpoint req))
`shouldBe` sort [("dry_run", Just "true"), ("timeout", Just "5s")]