packages feed

bloodhound-1.0.0.0: tests/Test/SearchOptionsSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.SearchOptionsSpec (spec) where

import Data.List (sort)
import Data.List.NonEmpty (NonEmpty ((:|)))
import Data.Maybe (isJust)
import Data.Text (Text)
import Database.Bloodhound.Common.Requests (searchOptionsParams)
import Database.Bloodhound.Common.Types
import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy)

-- | Stable ordering for equality: 'withQueries' preserves the order in
-- which params are emitted, but the tests compare the @Set@ of params to
-- avoid coupling to internal field ordering of 'SearchOptions'.
normalize :: [(Text, Maybe Text)] -> [(Text, Maybe Text)]
normalize = sort

spec :: Spec
spec =
  describe "SearchOptions URI param rendering" $ do
    it "defaultSearchOptions emits no params with the default SearchType" $ do
      searchOptionsParams defaultSearchOptions SearchTypeQueryThenFetch
        `shouldBe` []

    it "defaultSearchOptions + DfsQueryThenFetch emits only search_type" $ do
      searchOptionsParams defaultSearchOptions SearchTypeDfsQueryThenFetch
        `shouldBe` [("search_type", Just "dfs_query_then_fetch")]

    it "emits search_type even when other params are set" $ do
      let opts =
            defaultSearchOptions
              { soPreference = Just "_local"
              }
      normalize (searchOptionsParams opts SearchTypeDfsQueryThenFetch)
        `shouldBe` [ ("preference", Just "_local"),
                     ("search_type", Just "dfs_query_then_fetch")
                   ]

    it "renders every Bool field as true/false strings" $ do
      let opts =
            defaultSearchOptions
              { soAllowNoIndices = Just True,
                soIgnoreUnavailable = Just False,
                soRequestCache = Just True,
                soTypedKeys = Just False,
                soSeqNoPrimaryTerm = Just True,
                soCcsMinimizeRoundtrips = Just False,
                soAllowPartialSearchResults = Just True
              }
      normalize (searchOptionsParams opts SearchTypeQueryThenFetch)
        `shouldBe` [ ("allow_no_indices", Just "true"),
                     ("allow_partial_search_results", Just "true"),
                     ("ccs_minimize_roundtrips", Just "false"),
                     ("ignore_unavailable", Just "false"),
                     ("request_cache", Just "true"),
                     ("seq_no_primary_term", Just "true"),
                     ("typed_keys", Just "false")
                   ]

    it "renders every Int field as decimal strings" $ do
      let opts =
            defaultSearchOptions
              { soMaxConcurrentShardRequests = Just 16,
                soBatchedReduceSize = Just 512,
                soPreFilterShardSize = Just 128,
                soMaxConcurrentSearches = Just 4
              }
      normalize (searchOptionsParams opts SearchTypeQueryThenFetch)
        `shouldBe` [ ("batched_reduce_size", Just "512"),
                     ("max_concurrent_searches", Just "4"),
                     ("max_concurrent_shard_requests", Just "16"),
                     ("pre_filter_shard_size", Just "128")
                   ]

    it "renders expand_wildcards as a comma-separated list of values" $ do
      let opts =
            defaultSearchOptions
              { soExpandWildcards = Just (ExpandWildcardsOpen :| [ExpandWildcardsClosed])
              }
      searchOptionsParams opts SearchTypeQueryThenFetch
        `shouldBe` [("expand_wildcards", Just "open,closed")]

    it "renders a single expand_wildcards value without a trailing comma" $ do
      let opts =
            defaultSearchOptions
              { soExpandWildcards = Just (ExpandWildcardsAll :| [])
              }
      searchOptionsParams opts SearchTypeQueryThenFetch
        `shouldBe` [("expand_wildcards", Just "all")]

    it "renders preference and routing verbatim" $ do
      let opts =
            defaultSearchOptions
              { soPreference = Just "_only_local",
                soRouting = Just "route1,route2"
              }
      normalize (searchOptionsParams opts SearchTypeQueryThenFetch)
        `shouldBe` [ ("preference", Just "_only_local"),
                     ("routing", Just "route1,route2")
                   ]

    it "renders _source as a true/false string" $ do
      let opts = defaultSearchOptions {soSource = Just False}
      searchOptionsParams opts SearchTypeQueryThenFetch
        `shouldBe` [("_source", Just "false")]

    it "renders _source_includes and _source_excludes verbatim" $ do
      let opts =
            defaultSearchOptions
              { soSourceIncludes = Just "metadata.*,name",
                soSourceExcludes = Just "internal.*"
              }
      normalize (searchOptionsParams opts SearchTypeQueryThenFetch)
        `shouldBe` [ ("_source_excludes", Just "internal.*"),
                     ("_source_includes", Just "metadata.*,name")
                   ]

    it "renders cancel_after_time_interval as <n><unit>" $ do
      let opts =
            defaultSearchOptions
              { soCancelAfterTimeInterval = Just (TimeUnitSeconds, 5)
              }
      searchOptionsParams opts SearchTypeQueryThenFetch
        `shouldBe` [("cancel_after_time_interval", Just "5s")]

    it "supports every TimeUnits suffix" $ do
      let mk u = searchOptionsParams defaultSearchOptions {soCancelAfterTimeInterval = Just (u, 1)} SearchTypeQueryThenFetch
      mk TimeUnitDays `shouldBe` [("cancel_after_time_interval", Just "1d")]
      mk TimeUnitHours `shouldBe` [("cancel_after_time_interval", Just "1h")]
      mk TimeUnitMinutes `shouldBe` [("cancel_after_time_interval", Just "1m")]
      mk TimeUnitSeconds `shouldBe` [("cancel_after_time_interval", Just "1s")]
      mk TimeUnitMilliseconds `shouldBe` [("cancel_after_time_interval", Just "1ms")]
      mk TimeUnitMicroseconds `shouldBe` [("cancel_after_time_interval", Just "1micros")]
      mk TimeUnitNanoseconds `shouldBe` [("cancel_after_time_interval", Just "1nanos")]

    it "emits the full param set when every field is populated" $ do
      let opts =
            defaultSearchOptions
              { soAllowNoIndices = Just True,
                soExpandWildcards = Just (ExpandWildcardsOpen :| []),
                soIgnoreUnavailable = Just True,
                soPreference = Just "_local",
                soRouting = Just "r1",
                soRequestCache = Just False,
                soTypedKeys = Just True,
                soSeqNoPrimaryTerm = Just True,
                soMaxConcurrentShardRequests = Just 5,
                soBatchedReduceSize = Just 128,
                soCcsMinimizeRoundtrips = Just True,
                soAllowPartialSearchResults = Just False,
                soPreFilterShardSize = Just 128,
                soCancelAfterTimeInterval = Just (TimeUnitSeconds, 3),
                soMaxConcurrentSearches = Just 8,
                soSource = Just True,
                soSourceIncludes = Just "a,b",
                soSourceExcludes = Just "c"
              }
      -- Use DfsQueryThenFetch to verify search_type is included exactly
      -- once alongside the 18 SearchOptions keys, locking both the count
      -- and the exact wire key set.
      let rendered = searchOptionsParams opts SearchTypeDfsQueryThenFetch
      length rendered `shouldBe` 19
      rendered `shouldSatisfy` all (isJust . snd)
      sort (map fst rendered)
        `shouldBe` sort
          [ "_source",
            "_source_excludes",
            "_source_includes",
            "allow_no_indices",
            "allow_partial_search_results",
            "batched_reduce_size",
            "cancel_after_time_interval",
            "ccs_minimize_roundtrips",
            "expand_wildcards",
            "ignore_unavailable",
            "max_concurrent_searches",
            "max_concurrent_shard_requests",
            "pre_filter_shard_size",
            "preference",
            "request_cache",
            "routing",
            "search_type",
            "seq_no_primary_term",
            "typed_keys"
          ]