bloodhound-1.0.0.0: tests/Test/FeaturesSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module Test.FeaturesSpec (spec) where
import Data.Aeson
import Data.ByteString.Lazy.Char8 qualified as LBS
import Data.Text qualified as T
import Database.Bloodhound.Client.Cluster (BackendType (..))
import Database.Bloodhound.ElasticSearch9.Client qualified as ClientES9
import Database.Bloodhound.ElasticSearch9.Requests qualified as RequestsES9
import Database.Bloodhound.ElasticSearch9.Types qualified as ES9
import System.Environment (lookupEnv)
import TestsUtils.Common
import TestsUtils.Import
import Prelude
------------------------------------------------------------------------------
-- Sample payloads (trimmed to the wire fields decoded by the types).
------------------------------------------------------------------------------
-- | @GET /_features@ response matching the documented example.
sampleGetFeaturesBytes :: LBS.ByteString
sampleGetFeaturesBytes =
"{\
\ \"features\": [\
\ {\"name\": \"tasks\", \"description\": \"Manages task results\"},\
\ {\"name\": \"kibana\", \"description\": \"Manages Kibana configuration and reports\"}\
\ ]\
\}"
-- | @POST /_features/_reset@ response matching the documented example,
-- which uses @feature_name@/@status@ (not the @name@/@description@ the
-- schema declares).
sampleResetExampleBytes :: LBS.ByteString
sampleResetExampleBytes =
"{\
\ \"features\": [\
\ {\"feature_name\": \"security\", \"status\": \"SUCCESS\"},\
\ {\"feature_name\": \"tasks\", \"status\": \"SUCCESS\"}\
\ ]\
\}"
-- | The schema-declared @name@/@description@ shape, which the reset
-- endpoint may also return.
sampleResetSchemaBytes :: LBS.ByteString
sampleResetSchemaBytes =
"{\
\ \"features\": [\
\ {\"name\": \"tasks\", \"description\": \"Manages task results\"}\
\ ]\
\}"
spec :: Spec
spec = do
--------------------------------------------------------------------------
-- JSON (de)serialisation
--------------------------------------------------------------------------
describe "FeaturesResponse JSON" $ do
it "decodes the GET /_features example" $ do
let decoded = eitherDecode sampleGetFeaturesBytes :: Either String ES9.FeaturesResponse
decoded `shouldSatisfy` isRight
let Right resp = decoded
length (ES9.featuresResponseFeatures resp) `shouldBe` 2
it "round-trips through encode/decode" $ do
let decoded = eitherDecode sampleGetFeaturesBytes :: Either String ES9.FeaturesResponse
Right resp = decoded
eitherDecode (encode resp) `shouldBe` Right resp
describe "ResetFeaturesResponse JSON" $ do
it "decodes the documented feature_name/status example" $ do
let decoded = eitherDecode sampleResetExampleBytes :: Either String ES9.ResetFeaturesResponse
decoded `shouldSatisfy` isRight
let Right resp = decoded
length (ES9.resetFeaturesResponseFeatures resp) `shouldBe` 2
it "decodes the schema-declared name/description shape" $ do
let decoded = eitherDecode sampleResetSchemaBytes :: Either String ES9.ResetFeaturesResponse
decoded `shouldSatisfy` isRight
let Right resp = decoded
[item] = ES9.resetFeaturesResponseFeatures resp
ES9.rfiName item `shouldBe` Just "tasks"
--------------------------------------------------------------------------
-- Endpoint shape
--------------------------------------------------------------------------
describe "getFeatures endpoint shape" $ do
let req = RequestsES9.getFeatures
it "GETs /_features" $ do
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_features"]
it "uses the GET method" $ do
bhRequestMethod req `shouldBe` "GET"
it "carries no request body" $ do
bhRequestBody req `shouldBe` Nothing
it "carries no query string by default" $ do
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "renders master_timeout via getFeaturesWith" $ do
let opts = ES9.defaultFeaturesOptions {ES9.foMasterTimeout = Just (TimeUnitSeconds, 30)}
reqWith = RequestsES9.getFeaturesWith opts
getRawEndpointQueries (bhRequestEndpoint reqWith)
`shouldBe` [("master_timeout", Just "30s")]
describe "resetFeatures endpoint shape" $ do
let req = RequestsES9.resetFeatures
it "POSTs /_features/_reset" $ do
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_features", "_reset"]
it "uses the POST method" $ do
bhRequestMethod req `shouldBe` "POST"
it "sends an empty body" $ do
bhRequestBody req `shouldBe` Just ""
it "carries no query string by default" $ do
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
--------------------------------------------------------------------------
-- Live integration (ES9 only; the mutating reset is gated behind
-- ES_TEST_RUN_MUTATING so cluster state is not perturbed by default).
--------------------------------------------------------------------------
backendSpecific
[ElasticSearch9]
$ describe "Features API (live integration)"
$ do
it "getFeatures returns a parseable response" $
withTestEnv $ do
result <- tryEsError ClientES9.getFeatures
liftIO $
case result of
Left e
| endpointMissing e ->
pendingWith "features endpoints require Elasticsearch 9+"
_ -> pure ()
case result of
Left e -> liftIO $ expectationFailure $ "getFeatures failed: " <> show e
Right resp ->
liftIO $
ES9.featuresResponseFeatures resp `shouldSatisfy` (not . null)
it "resetFeatures returns a parseable response when ES_TEST_RUN_MUTATING=true" $
withTestEnv $ do
enabled <- liftIO $ lookupEnv "ES_TEST_RUN_MUTATING"
case enabled of
Just "true" -> do
result <- tryEsError ClientES9.resetFeatures
liftIO $
case result of
Left e
| endpointMissing e ->
pendingWith "features endpoints require Elasticsearch 9+"
_ -> pure ()
case result of
Left e -> liftIO $ expectationFailure $ "resetFeatures failed: " <> show e
Right resp ->
liftIO $
ES9.resetFeaturesResponseFeatures resp `shouldSatisfy` (not . null)
_ ->
liftIO $
pendingWith
"skipped: set ES_TEST_RUN_MUTATING=true to run this mutating test"
-- | Detect the @no handler found for uri@shape returned by Elasticsearch
-- when an endpoint isn't registered on this version. Used to mark the
-- live tests as 'pendingWith' rather than failing.
endpointMissing :: EsError -> Bool
endpointMissing e =
"no handler found" `T.isInfixOf` T.toLower (errorMessage e)