bloodhound-1.0.0.0: tests/Test/LogstashSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module Test.LogstashSpec (spec) where
import Data.Aeson
import Data.Aeson.KeyMap qualified as KM
import Data.ByteString.Lazy.Char8 qualified as LBS
import Data.Map.Strict qualified as M
import TestsUtils.Import
import Prelude
-- | A canonical pipeline body matching the ES 7.17 docs example: a
-- description, the pipeline DSL string, and metadata.
samplePipelineBytes :: LBS.ByteString
samplePipelineBytes =
"{\
\ \"description\": \"Sample pipeline\",\
\ \"pipeline\": \"input { stdin { } } output { stdout { } }\",\
\ \"pipeline_metadata\": {\
\ \"type\": \"logstash_pipeline\",\
\ \"version\": 1\
\ }\
\}"
-- | A pipeline carrying an optional @pipeline_settings@ map, a server-set
-- @last_modified@ (ES 7.x epoch-millis shape) and an unknown sibling field
-- (@@username_extra@) which the @lpcExtras@ catch-all must preserve.
samplePipelineWithExtrasBytes :: LBS.ByteString
samplePipelineWithExtrasBytes =
"{\
\ \"description\": \"with settings\",\
\ \"pipeline\": \"input { } output { }\",\
\ \"pipeline_settings\": {\
\ \"pipeline.batch.size\": \"125\",\
\ \"pipeline.batch.delay\": \"50\"\
\ },\
\ \"username\": \"elastic\",\
\ \"last_modified\": 1719394582880,\
\ \"username_extra\": true\
\}"
-- | Full @GET /_logstash/pipeline@ response: a bare object keyed by pipeline
-- id, with two entries.
sampleListResponseBytes :: LBS.ByteString
sampleListResponseBytes =
"{\
\ \"my-pipeline\": {\
\ \"description\": \"first\",\
\ \"pipeline\": \"input { stdin { } } output { stdout { } }\"\
\ },\
\ \"other-pipeline\": {\
\ \"description\": \"second\",\
\ \"pipeline\": \"input { beats { } } output { elasticsearch { } }\",\
\ \"pipeline_metadata\": {\"type\": \"logstash_pipeline\", \"version\": 2}\
\ }\
\}"
-- | @GET /_logstash/pipeline/{id}@ response shape: still a bare object, but
-- with a single entry keyed by the requested id.
sampleSingleResponseBytes :: LBS.ByteString
sampleSingleResponseBytes =
"{\
\ \"my-pipeline\": {\
\ \"description\": \"only one\",\
\ \"pipeline\": \"input { stdin { } } output { stdout { } }\"\
\ }\
\}"
spec :: Spec
spec = describe "Logstash pipeline API" $ do
describe "LogstashPipelineId JSON" $ do
it "round-trips as a bare JSON string" $ do
let Just decoded = decode "\"my-pipeline\"" :: Maybe LogstashPipelineId
unLogstashPipelineId decoded `shouldBe` "my-pipeline"
encode (LogstashPipelineId "my-pipeline") `shouldBe` "\"my-pipeline\""
it "rejects a non-string value" $ do
let decoded = decode "42" :: Maybe LogstashPipelineId
decoded `shouldBe` Nothing
describe "LogstashPipelineConfig JSON (PUT body)" $ do
it "decodes a canonical pipeline body" $ do
let Just decoded = decode samplePipelineBytes :: Maybe LogstashPipelineConfig
lpcDescription decoded `shouldBe` Just "Sample pipeline"
lpcPipeline decoded
`shouldBe` Just "input { stdin { } } output { stdout { } }"
-- The metadata body is carried as an opaque Value.
lpcPipelineMetadata decoded `shouldSatisfy` isJust
lpcPipelineSettings decoded `shouldBe` Nothing
lpcUsername decoded `shouldBe` Nothing
lpcLastModified decoded `shouldBe` Nothing
lpcExtras decoded `shouldBe` KM.empty
it "decodes settings, username, last_modified and preserves extras" $ do
let Just decoded = decode samplePipelineWithExtrasBytes :: Maybe LogstashPipelineConfig
lpcDescription decoded `shouldBe` Just "with settings"
lpcPipelineSettings decoded `shouldSatisfy` isJust
lpcUsername decoded `shouldBe` Just "elastic"
-- @last_modified@ is carried as an opaque epoch-millis Value.
lpcLastModified decoded `shouldSatisfy` isJust
-- The unknown @username_extra@ key survives in the extras catch-all.
KM.lookup "username_extra" (lpcExtras decoded) `shouldSatisfy` isJust
it "round-trips a pipeline with extras through encode . decode" $ do
let Just decoded = decode samplePipelineWithExtrasBytes :: Maybe LogstashPipelineConfig
Just roundTripped = decode (encode decoded) :: Maybe LogstashPipelineConfig
-- The @username_extra@ extra survives the round-trip.
KM.lookup "username_extra" (lpcExtras roundTripped) `shouldSatisfy` isJust
-- The known keys are NOT duplicated into extras.
KM.lookup "username" (lpcExtras roundTripped) `shouldBe` Nothing
KM.lookup "pipeline" (lpcExtras roundTripped) `shouldBe` Nothing
-- Typed fields survive too.
lpcUsername roundTripped `shouldBe` Just "elastic"
lpcDescription roundTripped `shouldBe` Just "with settings"
it "decodes an empty object into all-Nothing fields" $ do
let Just decoded = decode "{}" :: Maybe LogstashPipelineConfig
lpcDescription decoded `shouldBe` Nothing
lpcPipeline decoded `shouldBe` Nothing
lpcExtras decoded `shouldBe` KM.empty
it "preserves a null-valued extra through encode . decode" $ do
-- The 'object' (not 'omitNulls') encoder is chosen so an extra
-- carrying @null@ survives the round-trip — locking that guarantee.
let Just decoded =
decode
"{\"description\":\"d\",\"unknown_null\":null}" ::
Maybe LogstashPipelineConfig
Just roundTripped = decode (encode decoded) :: Maybe LogstashPipelineConfig
KM.lookup "unknown_null" (lpcExtras roundTripped) `shouldBe` Just Null
describe "LogstashPipelinesResponse JSON (GET response)" $ do
it "decodes the bare multi-entry object shape" $ do
let Just decoded =
decode sampleListResponseBytes :: Maybe LogstashPipelinesResponse
M.size (unLogstashPipelinesResponse decoded) `shouldBe` 2
let Just firstCfg =
M.lookup "my-pipeline" (unLogstashPipelinesResponse decoded)
lpcDescription firstCfg `shouldBe` Just "first"
let Just secondCfg =
M.lookup "other-pipeline" (unLogstashPipelinesResponse decoded)
-- The metadata body round-trips opaquely.
lpcPipelineMetadata secondCfg `shouldSatisfy` isJust
it "decodes the single-entry shape returned by GET /{id}" $ do
let Just decoded =
decode sampleSingleResponseBytes :: Maybe LogstashPipelinesResponse
M.size (unLogstashPipelinesResponse decoded) `shouldBe` 1
let Just cfg =
M.lookup "my-pipeline" (unLogstashPipelinesResponse decoded)
lpcDescription cfg `shouldBe` Just "only one"
it "re-encodes to the same bare-object shape and round-trips" $ do
let Just decoded =
decode sampleListResponseBytes :: Maybe LogstashPipelinesResponse
reEncoded = encode decoded
Just reparsed =
decode reEncoded :: Maybe LogstashPipelinesResponse
M.size (unLogstashPipelinesResponse reparsed) `shouldBe` 2
it "decodes an empty object to an empty map" $ do
let Just decoded = decode "{}" :: Maybe LogstashPipelinesResponse
unLogstashPipelinesResponse decoded `shouldBe` M.empty
it "rejects a non-object response" $ do
let decoded = decode "[]" :: Maybe LogstashPipelinesResponse
decoded `shouldBe` Nothing
describe "defaultLogstashPipelineConfig" $ do
it "builds an empty config" $ do
let cfg = defaultLogstashPipelineConfig
lpcDescription cfg `shouldBe` Nothing
lpcPipeline cfg `shouldBe` Nothing
lpcPipelineMetadata cfg `shouldBe` Nothing
lpcPipelineSettings cfg `shouldBe` Nothing
lpcUsername cfg `shouldBe` Nothing
lpcLastModified cfg `shouldBe` Nothing
lpcExtras cfg `shouldBe` KM.empty
it "serializes to the minimal empty PUT body" $ do
-- Every known field is Nothing, no extras -> empty object on the wire.
encode defaultLogstashPipelineConfig `shouldBe` "{}"