packages feed

bloodhound-1.0.0.0: tests/Test/SyncedFlushSpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# OPTIONS_GHC -Wno-incomplete-uni-patterns -Wno-warnings-deprecations #-}

module Test.SyncedFlushSpec (spec) where

import Data.Aeson.KeyMap qualified as KM
import Data.ByteString.Lazy qualified as LBS
import Data.List (sort)
import Database.Bloodhound.ElasticSearch7.Requests qualified as Requests
import Database.Bloodhound.ElasticSearch7.Types qualified as ES7Types
import Optics (set, view)
import TestsUtils.Common
import TestsUtils.Import

-- | Stable ordering for equality.
normalize :: [(Text, Maybe Text)] -> [(Text, Maybe Text)]
normalize = sort

-- | Documented success response: every shard sync-flushed cleanly, no
-- per-index @failures@ array.
sampleSuccessBytes :: LBS.ByteString
sampleSuccessBytes =
  "{\"_shards\":{\"total\":2,\"successful\":2,\"failed\":0},\
  \\"my-index-000001\":{\"total\":2,\"successful\":2,\"failed\":0}}"

-- | Documented partial-failure response: one shard group failed with a
-- per-shard @failures@ entry carrying a nested @routing@ object.
sampleFailureBytes :: LBS.ByteString
sampleFailureBytes =
  "{\"_shards\":{\"total\":4,\"successful\":1,\"failed\":1},\
  \\"my-index-000001\":{\"total\":4,\"successful\":3,\"failed\":1,\
  \\"failures\":[{\"shard\":1,\"reason\":\"unexpected error\",\
  \\"routing\":{\"state\":\"STARTED\",\"primary\":false,\
  \\"node\":\"SZNr2J_ORxKTLUCydGX4zA\",\"relocating_node\":null,\
  \\"shard\":1,\"index\":\"my-index-000001\"}}]}}"

spec :: Spec
spec = do
  -- ------------------------------------------------------------------ --
  -- Pure JSON decoder tests (no ES required).                          --
  -- ------------------------------------------------------------------ --
  describe "SyncedFlushResult JSON parsing" $ do
    it "peels _shards and collects the per-index shard groups" $ do
      let Just (r :: ES7Types.SyncedFlushResult) = decode sampleSuccessBytes
          shards = ES7Types.sfrShards r
      shardTotal shards `shouldBe` 2
      shardsSuccessful shards `shouldBe` 2
      case KM.lookup "my-index-000001" (ES7Types.sfrIndices r) of
        Just grp -> do
          ES7Types.sfsgTotal grp `shouldBe` 2
          ES7Types.sfsgSuccessful grp `shouldBe` 2
          ES7Types.sfsgFailed grp `shouldBe` 0
          ES7Types.sfsgFailures grp `shouldBe` []
        Nothing -> expectationFailure "expected my-index-000001 entry"

    it "parses a per-index failures array with nested routing" $ do
      let Just (r :: ES7Types.SyncedFlushResult) = decode sampleFailureBytes
          shards = ES7Types.sfrShards r
      shardsFailed shards `shouldBe` 1
      case KM.lookup "my-index-000001" (ES7Types.sfrIndices r) of
        Just grp -> do
          ES7Types.sfsgFailed grp `shouldBe` 1
          case ES7Types.sfsgFailures grp of
            [fail_] -> do
              ES7Types.sffShard fail_ `shouldBe` 1
              ES7Types.sffReason fail_ `shouldBe` "unexpected error"
              ES7Types.sffRouting fail_ `shouldNotBe` Nothing
            _ -> expectationFailure "expected exactly one failure"
        Nothing -> expectationFailure "expected my-index-000001 entry"

    it "round-trips the success response via ToJSON/FromJSON" $ do
      let Just (original :: ES7Types.SyncedFlushResult) = decode sampleSuccessBytes
          reDecoded = decode (encode original) :: Maybe ES7Types.SyncedFlushResult
      Just original `shouldBe` reDecoded

    it "round-trips the failure response via ToJSON/FromJSON" $ do
      let Just (original :: ES7Types.SyncedFlushResult) = decode sampleFailureBytes
          reDecoded = decode (encode original) :: Maybe ES7Types.SyncedFlushResult
      Just original `shouldBe` reDecoded

    it "tolerates a response with only _shards (empty index map)" $ do
      let Just (r :: ES7Types.SyncedFlushResult) =
            decode "{\"_shards\":{\"total\":0,\"successful\":0,\"failed\":0}}"
      ES7Types.sfrIndices r `shouldBe` mempty

    it "tolerates a per-index group missing the failures array" $ do
      let Just (r :: ES7Types.SyncedFlushResult) =
            decode
              "{\"_shards\":{\"total\":1,\"successful\":1,\"failed\":0},\
              \\"idx\":{\"total\":1,\"successful\":1,\"failed\":0}}"
      case KM.lookup "idx" (ES7Types.sfrIndices r) of
        Just grp -> ES7Types.sfsgFailures grp `shouldBe` []
        Nothing -> expectationFailure "expected idx entry"

  describe "SyncedFlushFailure JSON parsing" $ do
    it "parses a minimal failure with only shard and reason" $ do
      let Just (f :: ES7Types.SyncedFlushFailure) =
            decode "{\"shard\":3,\"reason\":\"[2] ongoing operations on primary\"}"
      ES7Types.sffShard f `shouldBe` 3
      ES7Types.sffReason f `shouldBe` "[2] ongoing operations on primary"
      ES7Types.sffRouting f `shouldBe` Nothing

    it "round-trips via ToJSON/FromJSON" $ do
      let Just (original :: ES7Types.SyncedFlushFailure) =
            decode "{\"shard\":1,\"reason\":\"x\"}"
          reDecoded = decode (encode original) :: Maybe ES7Types.SyncedFlushFailure
      Just original `shouldBe` reDecoded

  -- ------------------------------------------------------------------ --
  -- Optics.                                                            --
  -- ------------------------------------------------------------------ --
  describe "SyncedFlush lenses" $ do
    it "sfoIgnoreUnavailableLens is a lawful get/set" $ do
      let opts = set ES7Types.sfoIgnoreUnavailableLens (Just True) ES7Types.defaultSyncedFlushOptions
      view ES7Types.sfoIgnoreUnavailableLens opts `shouldBe` Just True

    it "sffShardLens is a lawful get/set" $ do
      let f = ES7Types.SyncedFlushFailure {ES7Types.sffShard = 0, ES7Types.sffReason = "", ES7Types.sffRouting = Nothing}
          f' = set ES7Types.sffShardLens 7 f
      view ES7Types.sffShardLens f' `shouldBe` (7 :: Int)

    it "sfsgFailedLens is a lawful get/set" $ do
      let g = ES7Types.SyncedFlushShardGroup {ES7Types.sfsgTotal = 0, ES7Types.sfsgSuccessful = 0, ES7Types.sfsgFailed = 0, ES7Types.sfsgFailures = []}
          g' = set ES7Types.sfsgFailedLens 2 g
      view ES7Types.sfsgFailedLens g' `shouldBe` (2 :: Int)

  -- ------------------------------------------------------------------ --
  -- Options rendering.                                                 --
  -- ------------------------------------------------------------------ --
  describe "SyncedFlushOptions URI param rendering" $ do
    it "defaultSyncedFlushOptions emits no query string" $
      ES7Types.syncedFlushOptionsParams ES7Types.defaultSyncedFlushOptions
        `shouldBe` []

    it "renders the optional params when set" $ do
      let opts =
            ES7Types.defaultSyncedFlushOptions
              { ES7Types.sfoIgnoreUnavailable = Just True,
                ES7Types.sfoAllowNoIndices = Just False,
                ES7Types.sfoExpandWildcards = Just (ExpandWildcardsOpen :| [])
              }
      normalize (ES7Types.syncedFlushOptionsParams opts)
        `shouldBe` [ ("allow_no_indices", Just "false"),
                     ("expand_wildcards", Just "open"),
                     ("ignore_unavailable", Just "true")
                   ]

  -- ------------------------------------------------------------------ --
  -- Endpoint shape (BHRequest, no ES required).                        --
  -- ------------------------------------------------------------------ --
  describe "syncedFlushIndex endpoint shape" $ do
    let idx = [qqIndexName|my-idx|]

    it "POSTs /{index}/_flush/synced with no query string by default" $ do
      let req = Requests.syncedFlushIndex idx
      bhRequestMethod req `shouldBe` "POST"
      getRawEndpoint (bhRequestEndpoint req)
        `shouldBe` ["my-idx", "_flush", "synced"]
      getRawEndpointQueries (bhRequestEndpoint req)
        `shouldBe` []

    it "sends an empty body" $ do
      let req = Requests.syncedFlushIndex idx
      bhRequestBody req `shouldBe` Just ""

    it "forwards options via the With variant" $ do
      let opts = ES7Types.defaultSyncedFlushOptions {ES7Types.sfoIgnoreUnavailable = Just True}
          req = Requests.syncedFlushIndexWith opts idx
      lookup "ignore_unavailable" (getRawEndpointQueries (bhRequestEndpoint req))
        `shouldBe` Just (Just "true")