packages feed

bloodhound-1.0.0.0: tests/Test/SimulateIngestSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.SimulateIngestSpec (spec) where

import Data.Aeson
import Data.Aeson.KeyMap qualified as KM
import Data.ByteString.Lazy.Char8 qualified as LBS
import Database.Bloodhound.ElasticSearch9.Requests qualified as RequestsES9
import Database.Bloodhound.ElasticSearch9.Types qualified as Types
import TestsUtils.Import

hasKey :: Key -> LBS.ByteString -> Bool
hasKey k bs = case decode bs of
  Just (Object obj) -> k `KM.member` obj
  _ -> False

-- | Extract the inner doc of the first response entry; the decode tests
-- feed known non-empty responses so the empty case is unreachable.
firstRespDoc :: Types.SimulateIngestResponse -> Types.SimulateIngestResponseInner
firstRespDoc r = case Types.sirResponseDocs r of
  (d : _) -> Types.srdDoc d
  [] -> error "firstRespDoc: empty SimulateIngestResponse"

spec :: Spec
spec =
  describe "Simulate ingest v2 API (GET/POST /_ingest/_simulate, /_ingest/{index}/_simulate)" $ do
    describe "SimulateIngestMergeType" $ do
      it "renders index/template as text" $ do
        Types.simulateIngestMergeTypeToText Types.SimulateIngestMergeIndex
          `shouldBe` ("index" :: Text)
        Types.simulateIngestMergeTypeToText Types.SimulateIngestMergeTemplate
          `shouldBe` ("template" :: Text)

      it "round-trips via fromText" $
        mapM_
          ( \mt ->
              Types.simulateIngestMergeTypeFromText
                (Types.simulateIngestMergeTypeToText mt)
                `shouldBe` Just mt
          )
          [minBound .. maxBound]

      it "rejects unknown values" $
        Types.simulateIngestMergeTypeFromText "bogus"
          `shouldBe` (Nothing :: Maybe Types.SimulateIngestMergeType)

    describe "simulateIngestOptionsParams URI rendering" $ do
      it "defaultSimulateIngestOptions emits no params" $
        Types.simulateIngestOptionsParams Types.defaultSimulateIngestOptions
          `shouldBe` []

      it "renders pipeline and merge_type" $ do
        let opts =
              Types.defaultSimulateIngestOptions
                { Types.sioPipeline = Just "my-pipeline",
                  Types.sioMergeType = Just Types.SimulateIngestMergeTemplate
                }
        Types.simulateIngestOptionsParams opts
          `shouldBe` [("pipeline", Just "my-pipeline"), ("merge_type", Just "template")]

      it "renders only the set field" $ do
        let opts =
              Types.defaultSimulateIngestOptions
                { Types.sioMergeType = Just Types.SimulateIngestMergeIndex
                }
        Types.simulateIngestOptionsParams opts
          `shouldBe` [("merge_type", Just "index")]

    describe "SimulateIngestDoc JSON" $ do
      it "encodes a doc, omitting optional id/index" $ do
        let doc =
              Types.SimulateIngestDoc
                { Types.sidId = Nothing,
                  Types.sidIndex = Nothing,
                  Types.sidSource = object ["foo" .= ("bar" :: Text)]
                }
        encode doc `shouldBe` "{\"_source\":{\"foo\":\"bar\"}}"

      it "encodes id and index when present" $ do
        let doc =
              Types.SimulateIngestDoc
                { Types.sidId = Just "123",
                  Types.sidIndex = Just "my-index",
                  Types.sidSource = object ["foo" .= ("bar" :: Text)]
                }
        decode (encode doc) `shouldBe` Just doc

      it "decodes the documented example" $ do
        let raw =
              LBS.pack
                "{\"_id\":\"123\",\"_index\":\"my-index\",\"_source\":{\"foo\":\"bar\"}}"
        case decode raw :: Maybe Types.SimulateIngestDoc of
          Just doc -> Types.sidId doc `shouldBe` Just "123"
          Nothing -> expectationFailure "failed to decode SimulateIngestDoc"

    describe "SimulateIngestRequest JSON" $ do
      it "encodes a minimal request (docs only)" $ do
        let req =
              Types.SimulateIngestRequest
                { Types.sirDocs =
                    [ Types.SimulateIngestDoc
                        { Types.sidId = Just "123",
                          Types.sidIndex = Just "my-index",
                          Types.sidSource = object ["foo" .= ("bar" :: Text)]
                        }
                    ],
                  Types.sirComponentTemplateSubstitutions = Nothing,
                  Types.sirIndexTemplateSubstitutions = Nothing,
                  Types.sirMappingAddition = Nothing
                }
        hasKey "component_template_substitutions" (encode req) `shouldBe` False
        decode (encode req) `shouldBe` Just req

      it "always emits the required docs key, even when empty" $ do
        let req =
              Types.SimulateIngestRequest
                { Types.sirDocs = [],
                  Types.sirComponentTemplateSubstitutions = Nothing,
                  Types.sirIndexTemplateSubstitutions = Nothing,
                  Types.sirMappingAddition = Nothing
                }
        hasKey "docs" (encode req) `shouldBe` True

      it "decodes a request, defaulting docs to []" $
        case decode (LBS.pack "{}") :: Maybe Types.SimulateIngestRequest of
          Just req -> length (Types.sirDocs req) `shouldBe` 0
          Nothing -> expectationFailure "failed to decode empty SimulateIngestRequest"

    describe "SimulateIngestResponse JSON" $ do
      it "decodes a documented response entry" $ do
        let raw =
              LBS.pack
                "{\"docs\":[{\"doc\":{\"_id\":\"123\",\"_index\":\"my-index\",\"_source\":{\"foo\":\"bar\"},\"executed_pipelines\":[\"my-pipeline\"]}}]}"
        case decode raw :: Maybe Types.SimulateIngestResponse of
          Just resp -> do
            length (Types.sirResponseDocs resp) `shouldBe` 1
            let inner = firstRespDoc resp
            Types.siriExecutedPipelines inner
              `shouldBe` ["my-pipeline"]
          Nothing -> expectationFailure "failed to decode SimulateIngestResponse"

      it "defaults executed_pipelines to [] when omitted" $ do
        let raw =
              LBS.pack
                "{\"docs\":[{\"doc\":{\"_id\":\"1\",\"_index\":\"i\",\"_source\":{}}}]}"
        case decode raw :: Maybe Types.SimulateIngestResponse of
          Just resp ->
            Types.siriExecutedPipelines
              (firstRespDoc resp)
              `shouldBe` []
          Nothing -> expectationFailure "failed to decode SimulateIngestResponse"

      it "round-trips an inner entry, preserving executed_pipelines" $ do
        let inner =
              Types.SimulateIngestResponseInner
                { Types.siriId = Just "1",
                  Types.siriIndex = Just "i",
                  Types.siriSource = Just (object ["foo" .= ("bar" :: Text)]),
                  Types.siriExecutedPipelines = [],
                  Types.siriIgnoredFields = Nothing,
                  Types.siriError = Nothing,
                  Types.siriEffectiveMapping = Nothing
                }
        hasKey "executed_pipelines" (encode inner) `shouldBe` True
        decode (encode inner) `shouldBe` Just inner

      it "decodes an entry carrying an error object" $ do
        let raw =
              LBS.pack
                "{\"docs\":[{\"doc\":{\"_source\":{},\"executed_pipelines\":[],\"error\":{\"type\":\"x\",\"reason\":\"y\"}}}]}"
        case decode raw :: Maybe Types.SimulateIngestResponse of
          Just resp ->
            Types.siriError
              (firstRespDoc resp)
              `shouldSatisfy` isJust
          Nothing -> expectationFailure "failed to decode SimulateIngestResponse"

    describe "endpoint shape" $ do
      it "POSTs /_ingest/_simulate with a body" $ do
        let req =
              RequestsES9.simulateIngest
                ( Types.SimulateIngestRequest
                    { Types.sirDocs = [],
                      Types.sirComponentTemplateSubstitutions = Nothing,
                      Types.sirIndexTemplateSubstitutions = Nothing,
                      Types.sirMappingAddition = Nothing
                    }
                )
        getRawEndpoint (bhRequestEndpoint req)
          `shouldBe` ["_ingest", "_simulate"]
        bhRequestBody req `shouldSatisfy` isJust
        getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []

      it "simulateIngestWith appends pipeline/merge_type params" $ do
        let opts =
              Types.defaultSimulateIngestOptions
                { Types.sioPipeline = Just "p"
                }
            req =
              RequestsES9.simulateIngestWith
                ( Types.SimulateIngestRequest
                    { Types.sirDocs = [],
                      Types.sirComponentTemplateSubstitutions = Nothing,
                      Types.sirIndexTemplateSubstitutions = Nothing,
                      Types.sirMappingAddition = Nothing
                    }
                )
                opts
        getRawEndpoint (bhRequestEndpoint req)
          `shouldBe` ["_ingest", "_simulate"]
        getRawEndpointQueries (bhRequestEndpoint req)
          `shouldBe` [("pipeline", Just "p")]

      it "POSTs /_ingest/<index>/_simulate" $
        case mkIndexName "my-index" of
          Left _ -> expectationFailure "mkIndexName rejected a valid name"
          Right idx -> do
            let req =
                  RequestsES9.simulateIngestIndex
                    idx
                    ( Types.SimulateIngestRequest
                        { Types.sirDocs = [],
                          Types.sirComponentTemplateSubstitutions = Nothing,
                          Types.sirIndexTemplateSubstitutions = Nothing,
                          Types.sirMappingAddition = Nothing
                        }
                    )
            getRawEndpoint (bhRequestEndpoint req)
              `shouldBe` ["_ingest", "my-index", "_simulate"]
            bhRequestBody req `shouldSatisfy` isJust

      it "simulateIngestIndexWith appends pipeline/merge_type params" $
        case mkIndexName "my-index" of
          Left _ -> expectationFailure "mkIndexName rejected a valid name"
          Right idx -> do
            let opts =
                  Types.defaultSimulateIngestOptions
                    { Types.sioMergeType = Just Types.SimulateIngestMergeIndex
                    }
                req =
                  RequestsES9.simulateIngestIndexWith
                    idx
                    ( Types.SimulateIngestRequest
                        { Types.sirDocs = [],
                          Types.sirComponentTemplateSubstitutions = Nothing,
                          Types.sirIndexTemplateSubstitutions = Nothing,
                          Types.sirMappingAddition = Nothing
                        }
                    )
                    opts
            getRawEndpoint (bhRequestEndpoint req)
              `shouldBe` ["_ingest", "my-index", "_simulate"]
            getRawEndpointQueries (bhRequestEndpoint req)
              `shouldBe` [("merge_type", Just "index")]

      it "GETs /_ingest/_simulate with a body" $ do
        let req =
              RequestsES9.simulateIngestGet
                ( Types.SimulateIngestRequest
                    { Types.sirDocs = [],
                      Types.sirComponentTemplateSubstitutions = Nothing,
                      Types.sirIndexTemplateSubstitutions = Nothing,
                      Types.sirMappingAddition = Nothing
                    }
                )
        getRawEndpoint (bhRequestEndpoint req)
          `shouldBe` ["_ingest", "_simulate"]
        -- GET-form still carries the required docs body (getWithBody).
        bhRequestBody req `shouldSatisfy` isJust
        getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []

      it "simulateIngestWithGet appends pipeline/merge_type params" $ do
        let opts =
              Types.defaultSimulateIngestOptions
                { Types.sioPipeline = Just "p"
                }
            req =
              RequestsES9.simulateIngestWithGet
                ( Types.SimulateIngestRequest
                    { Types.sirDocs = [],
                      Types.sirComponentTemplateSubstitutions = Nothing,
                      Types.sirIndexTemplateSubstitutions = Nothing,
                      Types.sirMappingAddition = Nothing
                    }
                )
                opts
        getRawEndpoint (bhRequestEndpoint req)
          `shouldBe` ["_ingest", "_simulate"]
        getRawEndpointQueries (bhRequestEndpoint req)
          `shouldBe` [("pipeline", Just "p")]

      it "GETs /_ingest/<index>/_simulate with a body" $
        case mkIndexName "my-index" of
          Left _ -> expectationFailure "mkIndexName rejected a valid name"
          Right idx -> do
            let req =
                  RequestsES9.simulateIngestIndexGet
                    idx
                    ( Types.SimulateIngestRequest
                        { Types.sirDocs = [],
                          Types.sirComponentTemplateSubstitutions = Nothing,
                          Types.sirIndexTemplateSubstitutions = Nothing,
                          Types.sirMappingAddition = Nothing
                        }
                    )
            getRawEndpoint (bhRequestEndpoint req)
              `shouldBe` ["_ingest", "my-index", "_simulate"]
            bhRequestBody req `shouldSatisfy` isJust

      it "simulateIngestIndexWithGet appends pipeline/merge_type params" $
        case mkIndexName "my-index" of
          Left _ -> expectationFailure "mkIndexName rejected a valid name"
          Right idx -> do
            let opts =
                  Types.defaultSimulateIngestOptions
                    { Types.sioMergeType = Just Types.SimulateIngestMergeIndex
                    }
                req =
                  RequestsES9.simulateIngestIndexWithGet
                    idx
                    ( Types.SimulateIngestRequest
                        { Types.sirDocs = [],
                          Types.sirComponentTemplateSubstitutions = Nothing,
                          Types.sirIndexTemplateSubstitutions = Nothing,
                          Types.sirMappingAddition = Nothing
                        }
                    )
                    opts
            getRawEndpoint (bhRequestEndpoint req)
              `shouldBe` ["_ingest", "my-index", "_simulate"]
            getRawEndpointQueries (bhRequestEndpoint req)
              `shouldBe` [("merge_type", Just "index")]