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"