packages feed

bloodhound-1.0.0.0: tests/Test/CountSpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}

module Test.CountSpec (spec) where

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

-- | 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 'CountOptions'.
normalize :: [(Text, Maybe Text)] -> [(Text, Maybe Text)]
normalize = sort

spec :: Spec
spec = do
  describe "CountOptions URI param rendering" $ do
    it "defaultCountOptions emits no params" $
      countOptionsParams defaultCountOptions `shouldBe` []

    it "renders every Bool field as true/false strings" $ do
      let opts =
            defaultCountOptions
              { coAllowNoIndices = Just True,
                coIgnoreUnavailable = Just False
              }
      normalize (countOptionsParams opts)
        `shouldBe` [ ("allow_no_indices", Just "true"),
                     ("ignore_unavailable", Just "false")
                   ]

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

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

    it "renders min_score as a decimal number" $ do
      let mk v = countOptionsParams defaultCountOptions {coMinScore = Just v}
      mk 0 `shouldBe` [("min_score", Just "0.0")]
      mk 1.5 `shouldBe` [("min_score", Just "1.5")]

    it "renders preference and routing verbatim" $ do
      let opts =
            defaultCountOptions
              { coPreference = Just "_only_local",
                coRouting = Just "route1,route2"
              }
      normalize (countOptionsParams opts)
        `shouldBe` [ ("preference", Just "_only_local"),
                     ("routing", Just "route1,route2")
                   ]

    it "emits the full param set when every field is populated" $ do
      let opts =
            defaultCountOptions
              { coAllowNoIndices = Just True,
                coExpandWildcards = Just (ExpandWildcardsOpen :| []),
                coIgnoreUnavailable = Just True,
                coMinScore = Just 0,
                coPreference = Just "_local",
                coRouting = Just "r1"
              }
      let rendered = countOptionsParams opts
      length rendered `shouldBe` 6
      rendered `shouldSatisfy` all (isJust . snd)
      sort (map fst rendered)
        `shouldBe` [ "allow_no_indices",
                     "expand_wildcards",
                     "ignore_unavailable",
                     "min_score",
                     "preference",
                     "routing"
                   ]

  describe "Count endpoint" $ do
    -- Pure JSON decoder tests for CountShards. The @skipped@ field was
    -- added in bloodhound-kc1 (mirroring ShardResult); older servers omit
    -- it, so the decoder defaults to 0. Pin both paths so the lenient
    -- parsing stays intact.
    it "parses CountShards with an explicit skipped field" $ do
      let Just (cs :: CountShards) =
            decode
              "{\"total\":2,\"successful\":2,\"skipped\":1,\"failed\":0}"
      cs `shouldBe` CountShards {csTotal = 2, csSuccessful = 2, csSkipped = 1, csFailed = 0}

    it "defaults CountShards.skipped to 0 when absent (legacy server)" $ do
      let Just (cs :: CountShards) =
            decode "{\"total\":1,\"successful\":1,\"failed\":0}"
      csSkipped cs `shouldBe` 0

    it "parses CountShards leniently when only total is present" $ do
      -- The server always returns total/successful/failed, but the
      -- decoder accepts the minimal shape too (defaults all to 0).
      let Just (cs :: CountShards) = decode "{\"total\":3}"
      csTotal cs `shouldBe` 3
      csSuccessful cs `shouldBe` 0
      csSkipped cs `shouldBe` 0
      csFailed cs `shouldBe` 0

    it "countByIndex returns count of a query" $
      withTestEnv $ do
        _ <- insertData
        let query = MatchAllQuery Nothing
            count = CountQuery query
        c <- performBHRequest $ countByIndex testIndex count
        liftIO $ crCount c `shouldBe` 1

    it "countAll returns a cluster-wide count without an index segment" $
      withTestEnv $ do
        _ <- insertData
        c <- performBHRequest $ countAll (CountQuery (MatchAllQuery Nothing))
        liftIO $ crCount c `shouldSatisfy` (>= 1)

    it "countByIndexWith Nothing matches countAll" $
      withTestEnv $ do
        _ <- insertData
        let q = CountQuery (MatchAllQuery Nothing)
        a <- performBHRequest $ countAll q
        b <- performBHRequest $ countByIndexWith Nothing defaultCountOptions q
        liftIO $ crCount a `shouldBe` crCount b

    it "countByIndexWith (Just []) matches countAll" $
      withTestEnv $ do
        _ <- insertData
        let q = CountQuery (MatchAllQuery Nothing)
        a <- performBHRequest $ countAll q
        b <- performBHRequest $ countByIndexWith (Just []) defaultCountOptions q
        liftIO $ crCount a `shouldBe` crCount b

    it "countByIndexWith (Just [testIndex]) matches countByIndex" $
      withTestEnv $ do
        _ <- insertData
        let q = CountQuery (MatchAllQuery Nothing)
        a <- performBHRequest $ countByIndex testIndex q
        b <- performBHRequest $ countByIndexWith (Just [testIndex]) defaultCountOptions q
        liftIO $ crCount a `shouldBe` crCount b

    it "countByIndexWith accepts multiple comma-joined indices" $
      withTestEnv $ do
        _ <- insertData
        c <-
          performBHRequest $
            countByIndexWith
              (Just [testIndex, testIndex])
              defaultCountOptions
              (CountQuery (MatchAllQuery Nothing))
        liftIO $ crCount c `shouldBe` 1

    it "countByIndexWith forwards ignore_unavailable for a missing index" $
      withTestEnv $ do
        _ <- insertData
        let missing = [qqIndexName|bloodhound-missing-index-count-spec|]
        -- With ignore_unavailable=true a missing index in the list is
        -- silently dropped, so the request succeeds and reports the
        -- count from the existing testIndex.
        c <-
          performBHRequest $
            countByIndexWith
              (Just [missing, testIndex])
              defaultCountOptions {coIgnoreUnavailable = Just True}
              (CountQuery (MatchAllQuery Nothing))
        liftIO $ crCount c `shouldBe` 1

    it "countByIndexWith forwards expand_wildcards against the test index" $
      withTestEnv $ do
        _ <- insertData
        c <-
          performBHRequest $
            countByIndexWith
              (Just [testIndex])
              defaultCountOptions
                { coExpandWildcards = Just (ExpandWildcardsOpen :| [])
                }
              (CountQuery (MatchAllQuery Nothing))
        liftIO $ crCount c `shouldBe` 1

    it "countByIndexWith forwards min_score without server rejection" $
      withTestEnv $ do
        _ <- insertData
        -- min_score=0 admits every document, so the live count must
        -- match the unfiltered countByIndex. This guards against a
        -- regression where the wrong URI key is sent (the server
        -- rejects unrecognised parameters with HTTP 400).
        baseline <- performBHRequest $ countByIndex testIndex (CountQuery (MatchAllQuery Nothing))
        c <-
          performBHRequest $
            countByIndexWith
              (Just [testIndex])
              defaultCountOptions {coMinScore = Just 0.0}
              (CountQuery (MatchAllQuery Nothing))
        liftIO $ crCount c `shouldBe` crCount baseline

    it "countByIndexWith forwards allow_no_indices, preference and routing together" $
      withTestEnv $ do
        _ <- insertData
        c <-
          performBHRequest $
            countByIndexWith
              (Just [testIndex])
              defaultCountOptions
                { coAllowNoIndices = Just True,
                  coPreference = Just "_local",
                  -- The test index is single-shard with no custom
                  -- routing, so any value here exercises the plumbing
                  -- without changing the result.
                  coRouting = Just "1"
                }
              (CountQuery (MatchAllQuery Nothing))
        liftIO $ crCount c `shouldBe` 1

    it "countByIndexWith surfaces an EsError for a missing index without ignore_unavailable" $ do
      let missing = [qqIndexName|bloodhound-missing-index-count-spec|]
      result <-
        withTestEnv $
          tryEsError
            ( performBHRequest $
                countByIndexWith
                  (Just [missing])
                  defaultCountOptions
                  (CountQuery (MatchAllQuery Nothing))
            )
      case result of
        Left _ -> pure ()
        Right _ -> expectationFailure "expected EsError for missing index, got success"