packages feed

bloodhound-1.0.0.0: tests/Test/IndexDocumentParamsSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.IndexDocumentParamsSpec (spec) where

import Data.Aeson (decode, encode)
import Data.List (sort)
import Data.Text (Text)
import Database.Bloodhound.Common.Requests (indexQueryString)
import Database.Bloodhound.Common.Types
import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy)

-- | Stable ordering for equality. 'indexQueryString' preserves the
-- order in which params are emitted (legacy version-control first,
-- then routing, then the newer ones), but tests should not couple to
-- that internal ordering.
normalize :: [(Text, Maybe Text)] -> [(Text, Maybe Text)]
normalize = sort

spec :: Spec
spec =
  describe "IndexDocumentSettings URI param rendering" $ do
    it "defaultIndexDocumentSettings emits no params" $ do
      indexQueryString defaultIndexDocumentSettings (DocId "1")
        `shouldBe` []

    it "JoinRelation ParentDocument emits routing = docId" $ do
      let cfg =
            defaultIndexDocumentSettings
              { idsJoinRelation =
                  Just (ParentDocument (FieldName "join") (RelationName "message"))
              }
      indexQueryString cfg (DocId "42")
        `shouldBe` [("routing", Just "42")]

    it "JoinRelation ChildDocument emits routing = parent docId" $ do
      let cfg =
            defaultIndexDocumentSettings
              { idsJoinRelation =
                  Just
                    ( ChildDocument
                        (FieldName "join")
                        (RelationName "reply")
                        (DocId "parent-1")
                    )
              }
      indexQueryString cfg (DocId "child-2")
        `shouldBe` [("routing", Just "parent-1")]

    it "ExternalGTE version control emits version + version_type" $ do
      let cfg =
            defaultIndexDocumentSettings
              { idsVersionControl = ExternalGTE (ExternalDocVersion minBound)
              }
      normalize (indexQueryString cfg (DocId "1"))
        `shouldBe` [ ("version", Just "1"),
                     ("version_type", Just "external_gte")
                   ]

    describe "op_type" $ do
      it "OpCreate renders as op_type=create" $ do
        let cfg = defaultIndexDocumentSettings {idsOpType = Just OpCreate}
        indexQueryString cfg (DocId "1")
          `shouldBe` [("op_type", Just "create")]
      it "OpIndex renders as op_type=index" $ do
        let cfg = defaultIndexDocumentSettings {idsOpType = Just OpIndex}
        indexQueryString cfg (DocId "1")
          `shouldBe` [("op_type", Just "index")]

    -- \| Regression coverage for the underlying 'OpType' type and the
    -- @idsOpType@ lens (the URI-rendering tests above exercise the
    -- downstream output only; these pin the type-level building blocks
    -- so a refactor of 'OpType' or its instances cannot silently break
    -- @op_type@ support). Bead bloodhound-04f.7.14.
    describe "OpType type and lens (regression)" $ do
      it "renderOpType yields the bare URI value" $ do
        renderOpType OpCreate `shouldBe` "create"
        renderOpType OpIndex `shouldBe` "index"
      it "ToJSON renders as the quoted string ES expects in bodies" $ do
        encode OpCreate `shouldBe` "\"create\""
        encode OpIndex `shouldBe` "\"index\""
      it "FromJSON round-trips both constructors" $ do
        decode "\"create\"" `shouldBe` Just OpCreate
        decode "\"index\"" `shouldBe` Just OpIndex
      it "FromJSON rejects unknown values" $ do
        (decode "\"upsert\"" :: Maybe OpType) `shouldBe` Nothing
      it "idsOpType field round-trips set/get" $ do
        let s = defaultIndexDocumentSettings {idsOpType = Just OpCreate}
        idsOpType s `shouldBe` Just OpCreate
      it "defaultIndexDocumentSettings has no op_type (Nothing)" $
        idsOpType defaultIndexDocumentSettings `shouldBe` Nothing

    it "pipeline renders the supplied pipeline id verbatim" $ do
      let cfg = defaultIndexDocumentSettings {idsPipeline = Just "my-pipeline"}
      indexQueryString cfg (DocId "1")
        `shouldBe` [("pipeline", Just "my-pipeline")]

    describe "refresh" $ do
      it "RefreshTrue renders as refresh=true" $ do
        let cfg = defaultIndexDocumentSettings {idsRefresh = Just RefreshTrue}
        indexQueryString cfg (DocId "1")
          `shouldBe` [("refresh", Just "true")]
      it "RefreshFalse renders as refresh=false" $ do
        let cfg = defaultIndexDocumentSettings {idsRefresh = Just RefreshFalse}
        indexQueryString cfg (DocId "1")
          `shouldBe` [("refresh", Just "false")]
      it "RefreshWaitFor renders as refresh=wait_for" $ do
        let cfg = defaultIndexDocumentSettings {idsRefresh = Just RefreshWaitFor}
        indexQueryString cfg (DocId "1")
          `shouldBe` [("refresh", Just "wait_for")]

    describe "wait_for_active_shards" $ do
      it "AllActiveShards renders as wait_for_active_shards=all" $ do
        let cfg =
              defaultIndexDocumentSettings
                { idsWaitForActiveShards = Just AllActiveShards
                }
        indexQueryString cfg (DocId "1")
          `shouldBe` [("wait_for_active_shards", Just "all")]
      it "ActiveShards n renders as wait_for_active_shards=<n>" $ do
        let cfg =
              defaultIndexDocumentSettings
                { idsWaitForActiveShards = Just (ActiveShards 3)
                }
        indexQueryString cfg (DocId "1")
          `shouldBe` [("wait_for_active_shards", Just "3")]

    describe "if_seq_no / if_primary_term" $ do
      it "emits both when both are set" $ do
        let cfg =
              defaultIndexDocumentSettings
                { idsIfSeqNo = Just 7,
                  idsIfPrimaryTerm = Just 2
                }
        normalize (indexQueryString cfg (DocId "1"))
          `shouldBe` [ ("if_primary_term", Just "2"),
                       ("if_seq_no", Just "7")
                     ]
      it "emits if_seq_no alone when only if_seq_no is set (pass-through to ES 400)" $ do
        let cfg = defaultIndexDocumentSettings {idsIfSeqNo = Just 7}
        indexQueryString cfg (DocId "1")
          `shouldBe` [("if_seq_no", Just "7")]
      it "emits if_primary_term alone when only if_primary_term is set (pass-through to ES 400)" $ do
        let cfg = defaultIndexDocumentSettings {idsIfPrimaryTerm = Just 2}
        indexQueryString cfg (DocId "1")
          `shouldBe` [("if_primary_term", Just "2")]

    describe "require_alias" $ do
      it "Just True renders as require_alias=true" $ do
        let cfg = defaultIndexDocumentSettings {idsRequireAlias = Just True}
        indexQueryString cfg (DocId "1")
          `shouldBe` [("require_alias", Just "true")]
      it "Just False renders as require_alias=false" $ do
        let cfg = defaultIndexDocumentSettings {idsRequireAlias = Just False}
        indexQueryString cfg (DocId "1")
          `shouldBe` [("require_alias", Just "false")]

    describe "timeout" $ do
      it "(TimeUnitSeconds, 30) renders as timeout=30s" $ do
        let cfg = defaultIndexDocumentSettings {idsTimeout = Just (TimeUnitSeconds, 30)}
        indexQueryString cfg (DocId "1")
          `shouldBe` [("timeout", Just "30s")]
      it "(TimeUnitMilliseconds, 200) renders as timeout=200ms" $ do
        let cfg = defaultIndexDocumentSettings {idsTimeout = Just (TimeUnitMilliseconds, 200)}
        indexQueryString cfg (DocId "1")
          `shouldBe` [("timeout", Just "200ms")]
      it "(TimeUnitMinutes, 1) renders as timeout=1m (matches server default shape)" $ do
        let cfg = defaultIndexDocumentSettings {idsTimeout = Just (TimeUnitMinutes, 1)}
        indexQueryString cfg (DocId "1")
          `shouldBe` [("timeout", Just "1m")]
      it "defaultIndexDocumentSettings has no timeout (Nothing)" $
        idsTimeout defaultIndexDocumentSettings `shouldBe` Nothing
      it "idsTimeout field round-trips set/get" $ do
        let s = defaultIndexDocumentSettings {idsTimeout = Just (TimeUnitSeconds, 5)}
        idsTimeout s `shouldBe` Just (TimeUnitSeconds, 5)

    it "renders every field together when all are populated" $ do
      let cfg =
            defaultIndexDocumentSettings
              { idsOpType = Just OpCreate,
                idsPipeline = Just "p",
                idsRefresh = Just RefreshWaitFor,
                idsWaitForActiveShards = Just AllActiveShards,
                idsIfSeqNo = Just 10,
                idsIfPrimaryTerm = Just 1,
                idsRequireAlias = Just True,
                idsTimeout = Just (TimeUnitSeconds, 5)
              }
      let rendered = indexQueryString cfg (DocId "1")
      length rendered `shouldBe` 8
      -- every value is non-Nothing
      rendered `shouldSatisfy` all (isJust . snd)
      normalize rendered
        `shouldBe` sort
          [ ("if_primary_term", Just "1"),
            ("if_seq_no", Just "10"),
            ("op_type", Just "create"),
            ("pipeline", Just "p"),
            ("refresh", Just "wait_for"),
            ("require_alias", Just "true"),
            ("timeout", Just "5s"),
            ("wait_for_active_shards", Just "all")
          ]

    it "preserves backwards compatibility: legacy version + join emit only the old params" $ do
      let cfg =
            defaultIndexDocumentSettings
              { idsVersionControl = ExternalGTE (ExternalDocVersion minBound),
                idsJoinRelation =
                  Just (ParentDocument (FieldName "j") (RelationName "message"))
              }
      normalize (indexQueryString cfg (DocId "abc"))
        `shouldBe` [ ("routing", Just "abc"),
                     ("version", Just "1"),
                     ("version_type", Just "external_gte")
                   ]
  where
    isJust (Just _) = True
    isJust Nothing = False