packages feed

bloodhound-1.0.0.0: tests/Test/SearchableSnapshotsSpec.hs

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

module Test.SearchableSnapshotsSpec (spec) where

import Data.Aeson
import Data.Aeson.KeyMap qualified as KM
import Data.ByteString.Lazy.Char8 qualified as LBS
import Database.Bloodhound.Common.Requests qualified as Common
import TestsUtils.Import
import Prelude

-- | A representative mount request body carrying the required @index@
-- plus the three documented optional fields.
sampleMountBodyBytes :: LBS.ByteString
sampleMountBodyBytes =
  "{\
  \  \"index\": \"fs\",\
  \  \"renamed_index\": \"fs-mounted\",\
  \  \"index_settings\": {\"index.number_of_replicas\": 0},\
  \  \"ignore_index_settings\": [\"index.refresh_interval\"]\
  \}"

spec :: Spec
spec = describe "Searchable Snapshots API" $ do
  describe "SearchableSnapshotMount JSON (POST body)" $ do
    it "decodes the documented fields" $ do
      let Just decoded = decode sampleMountBodyBytes :: Maybe SearchableSnapshotMount
      unIndexName (ssmIndex decoded) `shouldBe` "fs"
      ssmRenamedIndex decoded `shouldBe` Just "fs-mounted"
      ssmIgnoreIndexSettings decoded `shouldBe` Just ["index.refresh_interval"]

    it "requires the 'index' field" $ do
      let decoded = decode "{\"renamed_index\": \"x\"}" :: Maybe SearchableSnapshotMount
      decoded `shouldBe` Nothing

    it "tolerates missing optional fields" $ do
      let Just decoded = decode "{\"index\": \"only\"}" :: Maybe SearchableSnapshotMount
      unIndexName (ssmIndex decoded) `shouldBe` "only"
      ssmRenamedIndex decoded `shouldBe` Nothing
      ssmIgnoreIndexSettings decoded `shouldBe` Nothing
      ssmExtras decoded `shouldBe` KM.empty

    it "preserves unknown sibling fields in extras (forward-compatible)" $ do
      let bytes = encode (object ["index" .= String "idx", "future_field" .= (3 :: Int)])
          Just decoded = decode bytes :: Maybe SearchableSnapshotMount
      unIndexName (ssmIndex decoded) `shouldBe` "idx"
      KM.toList (ssmExtras decoded) `shouldSatisfy` any ((== "future_field") . fst)
      -- full round-trip must survive (extras carry through encode too)
      (decode . encode) decoded `shouldBe` Just decoded

    it "round-trips through ToJSON/FromJSON" $ do
      let Just decoded = decode sampleMountBodyBytes :: Maybe SearchableSnapshotMount
      (decode . encode) decoded `shouldBe` Just decoded

  describe "SearchableSnapshotStorage JSON" $ do
    it "round-trips the documented storage modes" $ do
      encode SearchableSnapshotStorageSharedPartialCache
        `shouldBe` "\"shared_partial_cache\""
      encode SearchableSnapshotStorageSharedCache
        `shouldBe` "\"shared_cache\""
      decode "\"shared_partial_cache\""
        `shouldBe` Just SearchableSnapshotStorageSharedPartialCache
      decode "\"shared_cache\""
        `shouldBe` Just SearchableSnapshotStorageSharedCache

    it "absorbs an unknown value into the custom branch" $ do
      let Just decoded = decode "\"future_mode\"" :: Maybe SearchableSnapshotStorage
      decoded `shouldBe` SearchableSnapshotStorageCustom "future_mode"

  describe "SearchableSnapshotMountOptions params" $ do
    it "default options render no query string" $ do
      searchableSnapshotMountOptionsParams defaultSearchableSnapshotMountOptions
        `shouldBe` []

    it "renders all three parameters when set" $ do
      let opts =
            defaultSearchableSnapshotMountOptions
              { ssmoMasterTimeout = Just "30s",
                ssmoWaitForCompletion = Just False,
                ssmoStorage = Just SearchableSnapshotStorageSharedCache
              }
      searchableSnapshotMountOptionsParams opts
        `shouldMatchList` [ ("master_timeout", Just "30s"),
                            ("wait_for_completion", Just "false"),
                            ("storage", Just "shared_cache")
                          ]

    it "omits Nothing fields" $ do
      let opts = defaultSearchableSnapshotMountOptions {ssmoWaitForCompletion = Just True}
      searchableSnapshotMountOptionsParams opts
        `shouldBe` [("wait_for_completion", Just "true")]

  describe "SearchableSnapshotMountResponse JSON" $ do
    it "decodes the wait_for_completion=true shape (snapshot object)" $ do
      let bytes = encode (object ["snapshot" .= object ["snapshot" .= String "snap-1"]])
          Just decoded = decode bytes :: Maybe SearchableSnapshotMountResponse
      ssmrSnapshot decoded `shouldSatisfy` isJust

    it "captures the wait_for_completion=false ack shape in extras" $ do
      let bytes =
            encode
              ( object
                  [ "acknowledged" .= True,
                    "shards_acknowledged" .= True
                  ]
              )
          Just decoded = decode bytes :: Maybe SearchableSnapshotMountResponse
      ssmrSnapshot decoded `shouldBe` Nothing
      KM.toList (ssmrExtras decoded)
        `shouldSatisfy` any ((== "acknowledged") . fst)

    it "round-trips through ToJSON/FromJSON" $ do
      let resp =
            SearchableSnapshotMountResponse
              { ssmrSnapshot = Just (object ["snapshot" .= String "s"]),
                ssmrExtras = KM.empty
              }
      (decode . encode) resp `shouldBe` Just resp

  describe "SearchableSnapshotsStatsResponse JSON" $ do
    it "decodes total/indices and tolerates absent nodes" $ do
      let bytes =
            encode
              ( object
                  [ "total" .= object ["file_cache_size" .= (1024 :: Int)],
                    "indices" .= object ["fs" .= object ["local" .= object []]]
                  ]
              )
          Just decoded = decode bytes :: Maybe SearchableSnapshotsStatsResponse
      sssrTotal decoded `shouldSatisfy` isJust
      sssrIndices decoded `shouldSatisfy` isJust
      sssrNodes decoded `shouldBe` Nothing

    it "round-trips through ToJSON/FromJSON" $ do
      let resp =
            SearchableSnapshotsStatsResponse
              { sssrTotal = Just (object []),
                sssrIndices = Just (KM.singleton "fs" (object [])),
                sssrNodes = Nothing,
                sssrExtras = KM.empty
              }
      (decode . encode) resp `shouldBe` Just resp

    it "preserves extras carrying null/empty-array values (forward-compat)" $ do
      -- omitNulls would silently drop these; 'object' must keep them.
      let resp =
            SearchableSnapshotsStatsResponse
              { sssrTotal = Nothing,
                sssrIndices = Nothing,
                sssrNodes = Nothing,
                sssrExtras =
                  KM.fromList
                    [ ("deprecated_field", Null),
                      ("empty_warnings", Array mempty)
                    ]
              }
      (decode . encode) resp `shouldBe` Just resp

  describe "SearchableSnapshotsCacheStatsResponse JSON" $ do
    it "decodes the indices map" $ do
      let bytes = encode (object ["indices" .= object ["fs" .= object []]])
          Just decoded = decode bytes :: Maybe SearchableSnapshotsCacheStatsResponse
      sscsrIndices decoded `shouldSatisfy` isJust

    it "round-trips through ToJSON/FromJSON" $ do
      let resp =
            SearchableSnapshotsCacheStatsResponse
              { sscsrIndices = Just (KM.singleton "fs" (object [])),
                sscsrExtras = KM.empty
              }
      (decode . encode) resp `shouldBe` Just resp

  describe "mountSearchableSnapshot endpoint shape" $ do
    let body = defaultSearchableSnapshotMount [qqIndexName|fs|]
        req = Common.mountSearchableSnapshot (SnapshotRepoName "my-repo") (SnapshotName "snap-1") body

    it "POSTs /_snapshot/{repo}/{snapshot}/_mount" $ do
      getRawEndpoint (bhRequestEndpoint req)
        `shouldBe` ["_snapshot", "my-repo", "snap-1", "_mount"]

    it "uses the POST method" $ do
      bhRequestMethod req `shouldBe` "POST"

    it "attaches the encoded body" $ do
      bhRequestBody req `shouldBe` Just (encode body)

    it "carries no query string by default" $ do
      getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []

    it "forwards options as query params in the With variant" $ do
      let opts = defaultSearchableSnapshotMountOptions {ssmoStorage = Just SearchableSnapshotStorageSharedCache}
          req' = Common.mountSearchableSnapshotWith (SnapshotRepoName "r") (SnapshotName "s") body opts
      getRawEndpointQueries (bhRequestEndpoint req')
        `shouldBe` [("storage", Just "shared_cache")]

  describe "getSearchableSnapshotsStats endpoint shape" $ do
    let req = Common.getSearchableSnapshotsStats

    it "GETs /_searchable_snapshots/stats" $ do
      getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_searchable_snapshots", "stats"]

    it "uses the GET method" $ do
      bhRequestMethod req `shouldBe` "GET"

    it "carries no request body" $ do
      bhRequestBody req `shouldBe` Nothing

  describe "getSearchableSnapshotsCacheStats endpoint shape" $ do
    let req = Common.getSearchableSnapshotsCacheStats [qqIndexName|fs|]

    it "POSTs /_searchable_snapshots/{index}/_cache_stats" $ do
      getRawEndpoint (bhRequestEndpoint req)
        `shouldBe` ["_searchable_snapshots", "fs", "_cache_stats"]

    it "uses the POST method" $ do
      bhRequestMethod req `shouldBe` "POST"

    it "sends an empty body" $ do
      bhRequestBody req `shouldBe` Just ""

  describe "clearSearchableSnapshotsCache endpoint shape" $ do
    let req = Common.clearSearchableSnapshotsCache [qqIndexName|fs|]

    it "POSTs /_searchable_snapshots/{index}/_clear_cache" $ do
      getRawEndpoint (bhRequestEndpoint req)
        `shouldBe` ["_searchable_snapshots", "fs", "_clear_cache"]

    it "uses the POST method (matches ES docs, not the bead's PUT)" $ do
      bhRequestMethod req `shouldBe` "POST"

    it "sends an empty body" $ do
      bhRequestBody req `shouldBe` Just ""