packages feed

bloodhound-1.0.0.0: tests/Test/DocumentOptionsSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.DocumentOptionsSpec (spec) where

import Data.Aeson (Value (..), encode, object, (.=))
import Data.List (sort)
import Data.Text qualified as T
import Database.Bloodhound.Common.Types
import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy)

-- | Stable ordering for equality. The renderers preserve field-declaration
-- order but tests should compare the set of params, not the order.
normalize :: [(T.Text, Maybe T.Text)] -> [(T.Text, Maybe T.Text)]
normalize = sort

-- | Boolean helpers rendered as @true@/@false@ strings.
boolTrue :: Maybe T.Text
boolTrue = Just "true"

boolFalse :: Maybe T.Text
boolFalse = Just "false"

spec :: Spec
spec = do
  describe "GetDocumentOptions URI param rendering" $ do
    it "defaultGetDocumentOptions emits no params" $
      getDocumentOptionsParams defaultGetDocumentOptions
        `shouldBe` []

    describe "_source" $ do
      it "Just True renders as _source=true" $ do
        let opts = defaultGetDocumentOptions {gdoSource = Just True}
        getDocumentOptionsParams opts `shouldBe` [("_source", boolTrue)]
      it "Just False renders as _source=false" $ do
        let opts = defaultGetDocumentOptions {gdoSource = Just False}
        getDocumentOptionsParams opts `shouldBe` [("_source", boolFalse)]

    it "_source_includes renders verbatim" $ do
      let opts = defaultGetDocumentOptions {gdoSourceIncludes = Just "metadata.*,name"}
      getDocumentOptionsParams opts
        `shouldBe` [("_source_includes", Just "metadata.*,name")]

    it "_source_excludes renders verbatim" $ do
      let opts = defaultGetDocumentOptions {gdoSourceExcludes = Just "internal.*"}
      getDocumentOptionsParams opts
        `shouldBe` [("_source_excludes", Just "internal.*")]

    it "stored_fields renders verbatim" $ do
      let opts = defaultGetDocumentOptions {gdoStoredFields = Just "user,location"}
      getDocumentOptionsParams opts
        `shouldBe` [("stored_fields", Just "user,location")]

    it "preference renders verbatim" $ do
      let opts = defaultGetDocumentOptions {gdoPreference = Just "_local"}
      getDocumentOptionsParams opts
        `shouldBe` [("preference", Just "_local")]

    describe "realtime" $ do
      it "Just True renders as realtime=true" $ do
        let opts = defaultGetDocumentOptions {gdoRealtime = Just True}
        getDocumentOptionsParams opts `shouldBe` [("realtime", boolTrue)]
      it "Just False renders as realtime=false" $ do
        let opts = defaultGetDocumentOptions {gdoRealtime = Just False}
        getDocumentOptionsParams opts `shouldBe` [("realtime", boolFalse)]

    describe "refresh" $ do
      it "Just True renders as refresh=true" $ do
        let opts = defaultGetDocumentOptions {gdoRefresh = Just True}
        getDocumentOptionsParams opts `shouldBe` [("refresh", boolTrue)]
      it "Just False renders as refresh=false" $ do
        let opts = defaultGetDocumentOptions {gdoRefresh = Just False}
        getDocumentOptionsParams opts `shouldBe` [("refresh", boolFalse)]

    it "routing renders verbatim" $ do
      let opts = defaultGetDocumentOptions {gdoRouting = Just "user-42"}
      getDocumentOptionsParams opts `shouldBe` [("routing", Just "user-42")]

    it "version renders the Word64 as a decimal string" $ do
      let opts = defaultGetDocumentOptions {gdoVersion = Just 7}
      getDocumentOptionsParams opts `shouldBe` [("version", Just "7")]

    describe "version_type" $ do
      it "renders internal" $ do
        let opts = defaultGetDocumentOptions {gdoVersionType = Just VersionTypeInternal}
        getDocumentOptionsParams opts
          `shouldBe` [("version_type", Just "internal")]
      it "renders external_gte" $ do
        let opts = defaultGetDocumentOptions {gdoVersionType = Just VersionTypeExternalGTE}
        getDocumentOptionsParams opts
          `shouldBe` [("version_type", Just "external_gte")]

    it "renders every field together when all are populated" $ do
      let opts =
            defaultGetDocumentOptions
              { gdoSource = Just False,
                gdoSourceIncludes = Just "a,b",
                gdoSourceExcludes = Just "c",
                gdoStoredFields = Just "s",
                gdoPreference = Just "_local",
                gdoRealtime = Just True,
                gdoRefresh = Just False,
                gdoRouting = Just "r",
                gdoVersion = Just 9,
                gdoVersionType = Just VersionTypeExternal
              }
      let rendered = getDocumentOptionsParams opts
      length rendered `shouldBe` 10
      rendered `shouldSatisfy` all (isJust . snd)
      normalize rendered
        `shouldBe` sort
          [ ("_source", Just "false"),
            ("_source_excludes", Just "c"),
            ("_source_includes", Just "a,b"),
            ("preference", Just "_local"),
            ("realtime", Just "true"),
            ("refresh", Just "false"),
            ("routing", Just "r"),
            ("stored_fields", Just "s"),
            ("version", Just "9"),
            ("version_type", Just "external")
          ]

  describe "GetDocumentSourceOptions URI param rendering" $ do
    it "defaultGetDocumentSourceOptions emits no params" $
      getDocumentSourceOptionsParams defaultGetDocumentSourceOptions
        `shouldBe` []

    it "_source_includes renders verbatim" $ do
      let opts = defaultGetDocumentSourceOptions {gdsoSourceIncludes = Just "metadata.*,name"}
      getDocumentSourceOptionsParams opts
        `shouldBe` [("_source_includes", Just "metadata.*,name")]

    it "_source_excludes renders verbatim" $ do
      let opts = defaultGetDocumentSourceOptions {gdsoSourceExcludes = Just "internal.*"}
      getDocumentSourceOptionsParams opts
        `shouldBe` [("_source_excludes", Just "internal.*")]

    it "preference renders verbatim" $ do
      let opts = defaultGetDocumentSourceOptions {gdsoPreference = Just "_local"}
      getDocumentSourceOptionsParams opts
        `shouldBe` [("preference", Just "_local")]

    describe "realtime" $ do
      it "Just True renders as realtime=true" $ do
        let opts = defaultGetDocumentSourceOptions {gdsoRealtime = Just True}
        getDocumentSourceOptionsParams opts `shouldBe` [("realtime", boolTrue)]
      it "Just False renders as realtime=false" $ do
        let opts = defaultGetDocumentSourceOptions {gdsoRealtime = Just False}
        getDocumentSourceOptionsParams opts `shouldBe` [("realtime", boolFalse)]

    describe "refresh" $ do
      it "Just True renders as refresh=true" $ do
        let opts = defaultGetDocumentSourceOptions {gdsoRefresh = Just True}
        getDocumentSourceOptionsParams opts `shouldBe` [("refresh", boolTrue)]
      it "Just False renders as refresh=false" $ do
        let opts = defaultGetDocumentSourceOptions {gdsoRefresh = Just False}
        getDocumentSourceOptionsParams opts `shouldBe` [("refresh", boolFalse)]

    it "routing renders verbatim" $ do
      let opts = defaultGetDocumentSourceOptions {gdsoRouting = Just "user-42"}
      getDocumentSourceOptionsParams opts `shouldBe` [("routing", Just "user-42")]

    it "version renders the Word64 as a decimal string" $ do
      let opts = defaultGetDocumentSourceOptions {gdsoVersion = Just 7}
      getDocumentSourceOptionsParams opts `shouldBe` [("version", Just "7")]

    describe "version_type" $ do
      it "renders internal" $ do
        let opts = defaultGetDocumentSourceOptions {gdsoVersionType = Just VersionTypeInternal}
        getDocumentSourceOptionsParams opts
          `shouldBe` [("version_type", Just "internal")]
      it "renders external_gte" $ do
        let opts = defaultGetDocumentSourceOptions {gdsoVersionType = Just VersionTypeExternalGTE}
        getDocumentSourceOptionsParams opts
          `shouldBe` [("version_type", Just "external_gte")]

    it "renders every field together when all are populated" $ do
      let opts =
            defaultGetDocumentSourceOptions
              { gdsoSourceIncludes = Just "a,b",
                gdsoSourceExcludes = Just "c",
                gdsoPreference = Just "_local",
                gdsoRealtime = Just True,
                gdsoRefresh = Just False,
                gdsoRouting = Just "r",
                gdsoVersion = Just 9,
                gdsoVersionType = Just VersionTypeExternal
              }
      let rendered = getDocumentSourceOptionsParams opts
      length rendered `shouldBe` 8
      rendered `shouldSatisfy` all (isJust . snd)
      normalize rendered
        `shouldBe` sort
          [ ("_source_excludes", Just "c"),
            ("_source_includes", Just "a,b"),
            ("preference", Just "_local"),
            ("realtime", Just "true"),
            ("refresh", Just "false"),
            ("routing", Just "r"),
            ("version", Just "9"),
            ("version_type", Just "external")
          ]

  describe "DeleteDocumentOptions URI param rendering" $ do
    it "defaultDeleteDocumentOptions emits no params" $
      deleteDocumentOptionsParams defaultDeleteDocumentOptions
        `shouldBe` []

    describe "wait_for_active_shards" $ do
      it "AllActiveShards renders as wait_for_active_shards=all" $ do
        let opts = defaultDeleteDocumentOptions {ddoWaitForActiveShards = Just AllActiveShards}
        deleteDocumentOptionsParams opts
          `shouldBe` [("wait_for_active_shards", Just "all")]
      it "ActiveShards n renders as wait_for_active_shards=<n>" $ do
        let opts = defaultDeleteDocumentOptions {ddoWaitForActiveShards = Just (ActiveShards 3)}
        deleteDocumentOptionsParams opts
          `shouldBe` [("wait_for_active_shards", Just "3")]

    describe "refresh" $ do
      it "RefreshTrue renders as refresh=true" $ do
        let opts = defaultDeleteDocumentOptions {ddoRefresh = Just RefreshTrue}
        deleteDocumentOptionsParams opts `shouldBe` [("refresh", Just "true")]
      it "RefreshFalse renders as refresh=false" $ do
        let opts = defaultDeleteDocumentOptions {ddoRefresh = Just RefreshFalse}
        deleteDocumentOptionsParams opts `shouldBe` [("refresh", Just "false")]
      it "RefreshWaitFor renders as refresh=wait_for" $ do
        let opts = defaultDeleteDocumentOptions {ddoRefresh = Just RefreshWaitFor}
        deleteDocumentOptionsParams opts `shouldBe` [("refresh", Just "wait_for")]

    it "routing renders verbatim" $ do
      let opts = defaultDeleteDocumentOptions {ddoRouting = Just "user-42"}
      deleteDocumentOptionsParams opts `shouldBe` [("routing", Just "user-42")]

    it "timeout renders verbatim" $ do
      let opts = defaultDeleteDocumentOptions {ddoTimeout = Just "5s"}
      deleteDocumentOptionsParams opts `shouldBe` [("timeout", Just "5s")]

    it "version renders the Word64 as a decimal string" $ do
      let opts = defaultDeleteDocumentOptions {ddoVersion = Just 7}
      deleteDocumentOptionsParams opts `shouldBe` [("version", Just "7")]

    describe "version_type" $ do
      it "renders internal" $ do
        let opts = defaultDeleteDocumentOptions {ddoVersionType = Just VersionTypeInternal}
        deleteDocumentOptionsParams opts
          `shouldBe` [("version_type", Just "internal")]
      it "renders external" $ do
        let opts = defaultDeleteDocumentOptions {ddoVersionType = Just VersionTypeExternal}
        deleteDocumentOptionsParams opts
          `shouldBe` [("version_type", Just "external")]
      it "renders external_gte" $ do
        let opts = defaultDeleteDocumentOptions {ddoVersionType = Just VersionTypeExternalGTE}
        deleteDocumentOptionsParams opts
          `shouldBe` [("version_type", Just "external_gte")]

    describe "if_seq_no / if_primary_term" $ do
      it "emits both when both are set" $ do
        let opts =
              defaultDeleteDocumentOptions
                { ddoIfSeqNo = Just 7,
                  ddoIfPrimaryTerm = Just 2
                }
        normalize (deleteDocumentOptionsParams opts)
          `shouldBe` [ ("if_primary_term", Just "2"),
                       ("if_seq_no", Just "7")
                     ]
      -- As with IndexDocumentSettings, each member of the OCC pair is
      -- emitted independently: a half-set pair is passed through to
      -- Elasticsearch's HTTP 400 rather than being silently dropped.
      it "emits if_seq_no alone when only if_seq_no is set" $ do
        let opts = defaultDeleteDocumentOptions {ddoIfSeqNo = Just 7}
        deleteDocumentOptionsParams opts
          `shouldBe` [("if_seq_no", Just "7")]
      it "emits if_primary_term alone when only if_primary_term is set" $ do
        let opts = defaultDeleteDocumentOptions {ddoIfPrimaryTerm = Just 2}
        deleteDocumentOptionsParams opts
          `shouldBe` [("if_primary_term", Just "2")]

    it "renders every field together when all are populated" $ do
      let opts =
            defaultDeleteDocumentOptions
              { ddoWaitForActiveShards = Just AllActiveShards,
                ddoRefresh = Just RefreshWaitFor,
                ddoRouting = Just "r",
                ddoTimeout = Just "5s",
                ddoVersion = Just 9,
                ddoVersionType = Just VersionTypeExternal,
                ddoIfSeqNo = Just 10,
                ddoIfPrimaryTerm = Just 1
              }
      let rendered = deleteDocumentOptionsParams opts
      length rendered `shouldBe` 8
      rendered `shouldSatisfy` all (isJust . snd)
      normalize rendered
        `shouldBe` sort
          [ ("if_primary_term", Just "1"),
            ("if_seq_no", Just "10"),
            ("refresh", Just "wait_for"),
            ("routing", Just "r"),
            ("timeout", Just "5s"),
            ("version", Just "9"),
            ("version_type", Just "external"),
            ("wait_for_active_shards", Just "all")
          ]

  describe "DocumentExistsOptions URI param rendering" $ do
    it "defaultDocumentExistsOptions emits no params" $
      documentExistsOptionsParams defaultDocumentExistsOptions
        `shouldBe` []

    it "stored_fields renders verbatim" $ do
      let opts = defaultDocumentExistsOptions {deoStoredFields = Just "user,location"}
      documentExistsOptionsParams opts
        `shouldBe` [("stored_fields", Just "user,location")]

    it "preference renders verbatim" $ do
      let opts = defaultDocumentExistsOptions {deoPreference = Just "_local"}
      documentExistsOptionsParams opts
        `shouldBe` [("preference", Just "_local")]

    describe "realtime" $ do
      it "Just True renders as realtime=true" $ do
        let opts = defaultDocumentExistsOptions {deoRealtime = Just True}
        documentExistsOptionsParams opts `shouldBe` [("realtime", boolTrue)]
      it "Just False renders as realtime=false" $ do
        let opts = defaultDocumentExistsOptions {deoRealtime = Just False}
        documentExistsOptionsParams opts `shouldBe` [("realtime", boolFalse)]

    describe "refresh" $ do
      it "Just True renders as refresh=true" $ do
        let opts = defaultDocumentExistsOptions {deoRefresh = Just True}
        documentExistsOptionsParams opts `shouldBe` [("refresh", boolTrue)]
      it "Just False renders as refresh=false" $ do
        let opts = defaultDocumentExistsOptions {deoRefresh = Just False}
        documentExistsOptionsParams opts `shouldBe` [("refresh", boolFalse)]

    it "routing renders verbatim" $ do
      let opts = defaultDocumentExistsOptions {deoRouting = Just "user-42"}
      documentExistsOptionsParams opts `shouldBe` [("routing", Just "user-42")]

    it "version renders the Word64 as a decimal string" $ do
      let opts = defaultDocumentExistsOptions {deoVersion = Just 7}
      documentExistsOptionsParams opts `shouldBe` [("version", Just "7")]

    describe "version_type" $ do
      it "renders internal" $ do
        let opts = defaultDocumentExistsOptions {deoVersionType = Just VersionTypeInternal}
        documentExistsOptionsParams opts
          `shouldBe` [("version_type", Just "internal")]
      it "renders external" $ do
        let opts = defaultDocumentExistsOptions {deoVersionType = Just VersionTypeExternal}
        documentExistsOptionsParams opts
          `shouldBe` [("version_type", Just "external")]
      it "renders external_gte" $ do
        let opts = defaultDocumentExistsOptions {deoVersionType = Just VersionTypeExternalGTE}
        documentExistsOptionsParams opts
          `shouldBe` [("version_type", Just "external_gte")]

    it "renders every field together when all are populated" $ do
      let opts =
            defaultDocumentExistsOptions
              { deoStoredFields = Just "s",
                deoPreference = Just "_local",
                deoRealtime = Just True,
                deoRefresh = Just False,
                deoRouting = Just "r",
                deoVersion = Just 9,
                deoVersionType = Just VersionTypeExternal
              }
      let rendered = documentExistsOptionsParams opts
      length rendered `shouldBe` 7
      rendered `shouldSatisfy` all (isJust . snd)
      normalize rendered
        `shouldBe` sort
          [ ("preference", Just "_local"),
            ("realtime", Just "true"),
            ("refresh", Just "false"),
            ("routing", Just "r"),
            ("stored_fields", Just "s"),
            ("version", Just "9"),
            ("version_type", Just "external")
          ]

  describe "DocumentSourceExistsOptions URI param rendering" $ do
    it "defaultDocumentSourceExistsOptions emits no params" $
      documentSourceExistsParams defaultDocumentSourceExistsOptions
        `shouldBe` []

    it "preference renders verbatim" $ do
      let opts = defaultDocumentSourceExistsOptions {dseoPreference = Just "_local"}
      documentSourceExistsParams opts
        `shouldBe` [("preference", Just "_local")]

    describe "realtime" $ do
      it "Just True renders as realtime=true" $ do
        let opts = defaultDocumentSourceExistsOptions {dseoRealtime = Just True}
        documentSourceExistsParams opts `shouldBe` [("realtime", boolTrue)]
      it "Just False renders as realtime=false" $ do
        let opts = defaultDocumentSourceExistsOptions {dseoRealtime = Just False}
        documentSourceExistsParams opts `shouldBe` [("realtime", boolFalse)]

    describe "refresh" $ do
      it "Just True renders as refresh=true" $ do
        let opts = defaultDocumentSourceExistsOptions {dseoRefresh = Just True}
        documentSourceExistsParams opts `shouldBe` [("refresh", boolTrue)]
      it "Just False renders as refresh=false" $ do
        let opts = defaultDocumentSourceExistsOptions {dseoRefresh = Just False}
        documentSourceExistsParams opts `shouldBe` [("refresh", boolFalse)]

    it "routing renders verbatim" $ do
      let opts = defaultDocumentSourceExistsOptions {dseoRouting = Just "user-42"}
      documentSourceExistsParams opts `shouldBe` [("routing", Just "user-42")]

    it "version renders the Word64 as a decimal string" $ do
      let opts = defaultDocumentSourceExistsOptions {dseoVersion = Just 7}
      documentSourceExistsParams opts `shouldBe` [("version", Just "7")]

    describe "version_type" $ do
      it "renders internal" $ do
        let opts = defaultDocumentSourceExistsOptions {dseoVersionType = Just VersionTypeInternal}
        documentSourceExistsParams opts
          `shouldBe` [("version_type", Just "internal")]
      it "renders external_gte" $ do
        let opts = defaultDocumentSourceExistsOptions {dseoVersionType = Just VersionTypeExternalGTE}
        documentSourceExistsParams opts
          `shouldBe` [("version_type", Just "external_gte")]

    it "renders every field together when all are populated" $ do
      let opts =
            defaultDocumentSourceExistsOptions
              { dseoPreference = Just "_local",
                dseoRealtime = Just True,
                dseoRefresh = Just False,
                dseoRouting = Just "r",
                dseoVersion = Just 9,
                dseoVersionType = Just VersionTypeExternal
              }
      let rendered = documentSourceExistsParams opts
      length rendered `shouldBe` 6
      rendered `shouldSatisfy` all (isJust . snd)
      normalize rendered
        `shouldBe` sort
          [ ("preference", Just "_local"),
            ("realtime", Just "true"),
            ("refresh", Just "false"),
            ("routing", Just "r"),
            ("version", Just "9"),
            ("version_type", Just "external")
          ]

  describe "ByQueryOptions URI param rendering" $ do
    it "defaultByQueryOptions emits no params" $
      byQueryOptionsParams defaultByQueryOptions
        `shouldBe` []

    describe "conflicts" $ do
      it "ConflictsProceed renders as conflicts=proceed" $ do
        let opts = defaultByQueryOptions {bqoConflicts = Just ConflictsProceed}
        byQueryOptionsParams opts `shouldBe` [("conflicts", Just "proceed")]
      it "ConflictsAbort renders as conflicts=abort" $ do
        let opts = defaultByQueryOptions {bqoConflicts = Just ConflictsAbort}
        byQueryOptionsParams opts `shouldBe` [("conflicts", Just "abort")]

    describe "wait_for_completion" $ do
      it "Just True renders as wait_for_completion=true" $ do
        let opts = defaultByQueryOptions {bqoWaitForCompletion = Just True}
        byQueryOptionsParams opts `shouldBe` [("wait_for_completion", boolTrue)]
      it "Just False renders as wait_for_completion=false" $ do
        let opts = defaultByQueryOptions {bqoWaitForCompletion = Just False}
        byQueryOptionsParams opts `shouldBe` [("wait_for_completion", boolFalse)]

    it "refresh renders via RefreshPolicy" $ do
      let opts = defaultByQueryOptions {bqoRefresh = Just RefreshTrue}
      byQueryOptionsParams opts `shouldBe` [("refresh", Just "true")]

    it "timeout renders verbatim" $ do
      let opts = defaultByQueryOptions {bqoTimeout = Just "5s"}
      byQueryOptionsParams opts `shouldBe` [("timeout", Just "5s")]

    it "scroll renders verbatim" $ do
      let opts = defaultByQueryOptions {bqoScroll = Just "1m"}
      byQueryOptionsParams opts `shouldBe` [("scroll", Just "1m")]

    it "scroll_size renders as decimal" $ do
      let opts = defaultByQueryOptions {bqoScrollSize = Just 500}
      byQueryOptionsParams opts `shouldBe` [("scroll_size", Just "500")]

    describe "slices" $ do
      it "SlicesAuto renders as slices=auto" $ do
        let opts = defaultByQueryOptions {bqoSlices = Just SlicesAuto}
        byQueryOptionsParams opts `shouldBe` [("slices", Just "auto")]
      it "SlicesCount n renders as slices=<n>" $ do
        let opts = defaultByQueryOptions {bqoSlices = Just (SlicesCount 4)}
        byQueryOptionsParams opts `shouldBe` [("slices", Just "4")]

    it "routing renders verbatim" $ do
      let opts = defaultByQueryOptions {bqoRouting = Just "user-1"}
      byQueryOptionsParams opts `shouldBe` [("routing", Just "user-1")]

    it "max_docs renders as decimal" $ do
      let opts = defaultByQueryOptions {bqoMaxDocs = Just 100}
      byQueryOptionsParams opts `shouldBe` [("max_docs", Just "100")]

    describe "request_cache" $ do
      it "Just True renders as request_cache=true" $ do
        let opts = defaultByQueryOptions {bqoRequestCache = Just True}
        byQueryOptionsParams opts `shouldBe` [("request_cache", boolTrue)]
      it "Just False renders as request_cache=false" $ do
        let opts = defaultByQueryOptions {bqoRequestCache = Just False}
        byQueryOptionsParams opts `shouldBe` [("request_cache", boolFalse)]

    it "pipeline renders verbatim" $ do
      let opts = defaultByQueryOptions {bqoPipeline = Just "my-pipe"}
      byQueryOptionsParams opts `shouldBe` [("pipeline", Just "my-pipe")]

    it "renders every field together when all are populated" $ do
      let opts =
            defaultByQueryOptions
              { bqoConflicts = Just ConflictsProceed,
                bqoWaitForCompletion = Just True,
                bqoRefresh = Just RefreshWaitFor,
                bqoTimeout = Just "5s",
                bqoScroll = Just "1m",
                bqoScrollSize = Just 100,
                bqoSlices = Just (SlicesCount 3),
                bqoRouting = Just "r",
                bqoMaxDocs = Just 50,
                bqoRequestCache = Just False,
                bqoPipeline = Just "p"
              }
      let rendered = byQueryOptionsParams opts
      length rendered `shouldBe` 11
      rendered `shouldSatisfy` all (isJust . snd)
      normalize rendered
        `shouldBe` sort
          [ ("conflicts", Just "proceed"),
            ("max_docs", Just "50"),
            ("pipeline", Just "p"),
            ("refresh", Just "wait_for"),
            ("request_cache", Just "false"),
            ("routing", Just "r"),
            ("scroll", Just "1m"),
            ("scroll_size", Just "100"),
            ("slices", Just "3"),
            ("timeout", Just "5s"),
            ("wait_for_completion", Just "true")
          ]

  describe "UpdateBody JSON encoding" $ do
    describe "UpdateDoc" $ do
      it "encodes as {\"doc\": ...} with no extras" $ do
        let body = mkUpdateDoc (object ["counter" .= (1 :: Int)])
        encode body
          `shouldBe` encode (object ["doc" .= object ["counter" .= (1 :: Int)]])
      it "includes doc_as_upsert when set to Just True" $ do
        let body =
              UpdateDoc
                { ubDoc = object ["name" .= String "x"],
                  ubDocAsUpsert = Just True
                }
        encode body
          `shouldBe` encode
            ( object
                [ "doc" .= object ["name" .= String "x"],
                  "doc_as_upsert" .= Bool True
                ]
            )
      it "omits doc_as_upsert when Nothing" $ do
        let body = mkUpdateDoc (object ["name" .= String "x"])
        encode body
          `shouldBe` encode (object ["doc" .= object ["name" .= String "x"]])
      it "omits doc_as_upsert when Just False is NOT set — Nothing is the only omit" $
        -- doc_as_upsert=Just False is a real value the caller might want to
        -- send; we should NOT collapse it. The omission is only for Nothing.
        do
          let body =
                UpdateDoc
                  { ubDoc = object ["name" .= String "x"],
                    ubDocAsUpsert = Just False
                  }
          encode body
            `shouldBe` encode
              ( object
                  [ "doc" .= object ["name" .= String "x"],
                    "doc_as_upsert" .= Bool False
                  ]
              )

    describe "UpdateScript" $ do
      it "encodes the script as a top-level \"script\" key without double-wrapping" $ do
        let script =
              Script
                { scriptLanguage = Just (ScriptLanguage "painless"),
                  scriptSource = ScriptInline "ctx._source.counter += 1",
                  scriptParams = Nothing,
                  scriptOptions = Nothing
                }
            body = mkUpdateScript script
        encode body
          `shouldBe` encode
            ( object
                [ "script"
                    .= object
                      [ "lang" .= String "painless",
                        "source" .= String "ctx._source.counter += 1"
                      ]
                ]
            )
      it "includes upsert when set" $ do
        let script =
              Script Nothing (ScriptInline "ctx._source.x = 1") Nothing Nothing
            body =
              UpdateScript
                { ubScript = script,
                  ubUpsert = Just (object ["x" .= (0 :: Int)]),
                  ubScriptedUpsert = Nothing
                }
        encode body
          `shouldBe` encode
            ( object
                [ "script" .= object ["source" .= String "ctx._source.x = 1"],
                  "upsert" .= object ["x" .= (0 :: Int)]
                ]
            )
      it "includes scripted_upsert when set to Just True" $ do
        let script =
              Script Nothing (ScriptInline "ctx._source.x = 1") Nothing Nothing
            body =
              UpdateScript
                { ubScript = script,
                  ubUpsert = Nothing,
                  ubScriptedUpsert = Just True
                }
        encode body
          `shouldBe` encode
            ( object
                [ "script" .= object ["source" .= String "ctx._source.x = 1"],
                  "scripted_upsert" .= Bool True
                ]
            )
      it "uses 'id' (not 'source') for stored scripts" $ do
        let script =
              Script Nothing (ScriptId "my-stored-script") Nothing Nothing
            body = mkUpdateScript script
        encode body
          `shouldBe` encode
            (object ["script" .= object ["id" .= String "my-stored-script"]])
  where
    isJust (Just _) = True
    isJust Nothing = False