bloodhound-1.0.0.0: tests/Test/SearchShardsSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# OPTIONS_GHC -Wno-incomplete-uni-patterns -Wno-x-partial #-}
module Test.SearchShardsSpec (spec) where
import Data.Aeson (Value (..), decode)
import Data.Aeson.KeyMap qualified as KM
import Data.ByteString.Lazy.Char8 qualified as BL8
import Data.Map.Strict qualified as M
import Database.Bloodhound.Common.Requests qualified as Requests
( getSearchShards,
getSearchShardsWith,
)
import TestsUtils.Common
import TestsUtils.Import
spec :: Spec
spec = do
describe "SearchShardsResponse JSON" $ do
it "decodes the canonical ES doc example with one STARTED primary" $
let bytes =
BL8.pack
"{\"nodes\":{\"CBH3C4RiQcK3CxFvPJyFtQ\":{\"name\":\"elasticsearch1\",\"ephemeral_id\":\"DGt8SykXTlWz3GEQtdNfUA\",\"transport_address\":\"172.19.0.6:9300\",\"attributes\":{\"ml.machine_memory\":\"2147483648\"},\"roles\":[\"data\",\"master\"]}},\"indices\":{\"bloodhound-tests-twitter-gettask-opts\":{}},\"shards\":[[{\"state\":\"STARTED\",\"primary\":true,\"node\":\"CBH3C4RiQcK3CxFvPJyFtQ\",\"relocating_node\":null,\"shard\":0,\"index\":\"bloodhound-tests-twitter-gettask-opts\",\"allocation_id\":{\"id\":\"A4BybQlTS_mWcfqMiOR2zg\"}}]]}"
Just resp = decode bytes
nodes = searchShardsResponseNodes resp
Just (firstShardCopies : _) = Just (searchShardsResponseShards resp)
firstShard = head firstShardCopies
Just nodeId = searchShardsShardNode firstShard
Just nodeInfo = M.lookup nodeId nodes
in do
M.size nodes `shouldBe` 1
searchShardsNodeName nodeInfo `shouldBe` Just "elasticsearch1"
searchShardsNodeTransportAddress nodeInfo `shouldBe` Just "172.19.0.6:9300"
searchShardsNodeRoles nodeInfo `shouldBe` Just ["data", "master"]
M.size (searchShardsResponseIndices resp) `shouldBe` 1
searchShardsShardState firstShard `shouldBe` SearchShardsStateStarted
searchShardsShardPrimary firstShard `shouldBe` True
searchShardsShardRelocatingNode firstShard `shouldBe` Nothing
searchShardsShardAllocationId firstShard
`shouldBe` Just (M.singleton "id" "A4BybQlTS_mWcfqMiOR2zg")
unIndexName (searchShardsShardIndex firstShard)
`shouldBe` "bloodhound-tests-twitter-gettask-opts"
it "decodes a response with empty nodes/indices/shards (no matching index)" $
let bytes = BL8.pack "{\"nodes\":{},\"indices\":{},\"shards\":[]}"
Just resp = decode bytes :: Maybe SearchShardsResponse
in do
M.null (searchShardsResponseNodes resp) `shouldBe` True
M.null (searchShardsResponseIndices resp) `shouldBe` True
searchShardsResponseShards resp `shouldBe` []
it "treats missing nodes/indices/shards fields as empty" $
let bytes = BL8.pack "{}"
Just resp = decode bytes :: Maybe SearchShardsResponse
in do
M.null (searchShardsResponseNodes resp) `shouldBe` True
M.null (searchShardsResponseIndices resp) `shouldBe` True
searchShardsResponseShards resp `shouldBe` []
it "decodes an unassigned shard (state=UNASSIGNED, node=null)" $
let bytes =
BL8.pack
"{\"nodes\":{},\"indices\":{\"foo\":{}},\"shards\":[[{\"state\":\"UNASSIGNED\",\"primary\":false,\"node\":null,\"relocating_node\":null,\"shard\":1,\"index\":\"foo\"}]]}"
Just resp = decode bytes :: Maybe SearchShardsResponse
Just (copy : _) = Just (searchShardsResponseShards resp)
shard = head copy
in do
searchShardsShardState shard `shouldBe` SearchShardsStateUnassigned
searchShardsShardNode shard `shouldBe` Nothing
searchShardsShardPrimary shard `shouldBe` False
it "preserves unknown sibling fields on a shard copy via searchShardsShardOther" $
let bytes =
BL8.pack
"{\"nodes\":{},\"indices\":{\"foo\":{}},\"shards\":[[{\"state\":\"STARTED\",\"primary\":true,\"node\":\"n1\",\"relocating_node\":null,\"shard\":0,\"index\":\"foo\",\"allocation_id\":{\"id\":\"x\"},\"future_field\":42}]]}"
Just resp = decode bytes :: Maybe SearchShardsResponse
Just (copy : _) = Just (searchShardsResponseShards resp)
shard = head copy
Just (Object other) = searchShardsShardOther shard
in KM.size other `shouldSatisfy` (>= 1)
it "preserves unknown sibling fields on a node via searchShardsNodeOther" $
let bytes =
BL8.pack
"{\"nodes\":{\"n1\":{\"name\":\"n\",\"future_attr\":\"x\"}},\"indices\":{},\"shards\":[]}"
Just resp = decode bytes :: Maybe SearchShardsResponse
Just nodeInfo = M.lookup "n1" (searchShardsResponseNodes resp)
Just (Object other) = searchShardsNodeOther nodeInfo
in do
searchShardsNodeName nodeInfo `shouldBe` Just "n"
KM.size other `shouldSatisfy` (>= 1)
it "round-trips an unknown shard state through SearchShardsStateOther" $
let bytes =
BL8.pack
"{\"nodes\":{},\"indices\":{\"foo\":{}},\"shards\":[[{\"state\":\"WEIRD\",\"primary\":true,\"node\":\"n1\",\"relocating_node\":null,\"shard\":0,\"index\":\"foo\"}]]}"
Just resp = decode bytes :: Maybe SearchShardsResponse
Just (copy : _) = Just (searchShardsResponseShards resp)
shard = head copy
in searchShardsShardState shard `shouldBe` SearchShardsStateOther "WEIRD"
describe "SearchShardsOptions params" $ do
it "renders nothing for defaultSearchShardsOptions" $
searchShardsOptionsParams defaultSearchShardsOptions `shouldBe` []
it "renders local as a boolean string" $
let opts = defaultSearchShardsOptions {sshoLocal = Just True}
in lookup "local" (searchShardsOptionsParams opts) `shouldBe` Just (Just "true")
it "renders allow_no_indices as a boolean string" $
let opts = defaultSearchShardsOptions {sshoAllowNoIndices = Just False}
in lookup "allow_no_indices" (searchShardsOptionsParams opts)
`shouldBe` Just (Just "false")
it "renders expand_wildcards comma-joined" $
let opts =
defaultSearchShardsOptions
{ sshoExpandWildcards = Just [ExpandWildcardsOpen, ExpandWildcardsClosed]
}
in lookup "expand_wildcards" (searchShardsOptionsParams opts)
`shouldBe` Just (Just "open,closed")
it "renders preference verbatim" $
let opts = defaultSearchShardsOptions {sshoPreference = Just "_primary"}
in lookup "preference" (searchShardsOptionsParams opts) `shouldBe` Just (Just "_primary")
it "renders routing comma-joined" $
let opts = defaultSearchShardsOptions {sshoRouting = Just ["a", "b"]}
in lookup "routing" (searchShardsOptionsParams opts) `shouldBe` Just (Just "a,b")
it "renders master_timeout via timeUnitsSuffix" $
let opts = defaultSearchShardsOptions {sshoMasterTimeout = Just (TimeUnitSeconds, 30)}
in lookup "master_timeout" (searchShardsOptionsParams opts) `shouldBe` Just (Just "30s")
it "omits master_timeout by default" $
lookup "master_timeout" (searchShardsOptionsParams defaultSearchShardsOptions)
`shouldBe` Nothing
describe "Search Shards endpoint shape" $ do
-- Pure checks against the BHRequest shape; no live backend needed.
it "POSTs /_search_shards with no body" $ do
let req = Requests.getSearchShards Nothing
bhRequestMethod req `shouldBe` "POST"
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_search_shards"]
bhRequestBody req `shouldBe` Nothing
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "treats Just [] the same as Nothing (single _search_shards segment)" $ do
let reqNo = Requests.getSearchShards Nothing
reqEmpty = Requests.getSearchShards (Just [])
getRawEndpoint (bhRequestEndpoint reqNo)
`shouldBe` getRawEndpoint (bhRequestEndpoint reqEmpty)
it "comma-joins a non-empty index list into the leading segment" $ do
let req = Requests.getSearchShards (Just [[qqIndexName|twitter-1|], [qqIndexName|twitter-2|]])
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["twitter-1,twitter-2", "_search_shards"]
it "honours SearchShardsOptions in the query string" $ do
let req =
Requests.getSearchShardsWith
(Just [[qqIndexName|twitter-1|]])
defaultSearchShardsOptions {sshoPreference = Just "_primary"}
getRawEndpointQueries (bhRequestEndpoint req)
`shouldBe` [("preference", Just "_primary")]
describe "Search Shards endpoint" $ do
it "returns at least one assigned primary shard for the test index" $
withTestEnv $ do
_ <- insertData
resp <- performBHRequest $ getSearchShards (Just [testIndex])
liftIO $ do
let shards = searchShardsResponseShards resp
shards `shouldSatisfy` (not . null)
let allShards = concat shards
allShards `shouldSatisfy` any searchShardsShardPrimary
let testIndexShards =
filter
((==) (unIndexName testIndex) . unIndexName . searchShardsShardIndex)
allShards
testIndexShards `shouldSatisfy` (not . null)
it "exposes a node entry for every assigned shard's node id" $
withTestEnv $ do
_ <- insertData
resp <- performBHRequest $ getSearchShards (Just [testIndex])
liftIO $ do
let nodes = searchShardsResponseNodes resp
assignedNodeIds =
[ nid
| copy <- concat (searchShardsResponseShards resp),
Just nid <- [searchShardsShardNode copy]
]
uniqueNodeIds = uniq assignedNodeIds
uniqueNodeIds `shouldSatisfy` (not . null)
mapM_ (\nid -> M.member nid nodes `shouldBe` True) uniqueNodeIds
it "passes preference and routing via SearchShardsOptions without error" $
withTestEnv $ do
_ <- insertData
resp <-
performBHRequest $
getSearchShardsWith
(Just [testIndex])
defaultSearchShardsOptions
{ sshoPreference = Just "_local",
sshoRouting = Just ["user_1"]
}
liftIO $
searchShardsResponseShards resp `shouldSatisfy` (not . null)
-- | Local helper because we cannot use the lens from the module under
-- test in two different roles easily without an explicit accessor.
uniq :: (Eq a) => [a] -> [a]
uniq = go []
where
go _ [] = []
go seen (x : xs)
| x `elem` seen = go seen xs
| otherwise = x : go (x : seen) xs