packages feed

bloodhound-1.0.0.0: tests/Test/ScrollSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.ScrollSpec (spec) where

import Data.Aeson
  ( decode,
    eitherDecode,
    encode,
  )
import Data.Aeson.Types (parseEither, parseJSON)
import Data.ByteString.Lazy.Char8 qualified as LBS
import Data.Either (isLeft)
import Data.Maybe (isJust)
import Database.Bloodhound.Common.Types
import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy)
import Prelude

-- Sample Clear Scroll response body, as documented by both Elasticsearch
-- and OpenSearch:
--   https://www.elastic.co/guide/en/elasticsearch/reference/current/clear-scroll-api.html
--   https://docs.opensearch.org/latest/api-reference/scroll-search/
sampleClearScrollResponseBody :: LBS.ByteString
sampleClearScrollResponseBody =
  LBS.pack
    ( unlines
        [ "{",
          "  \"succeeded\": true,",
          "  \"num_freed\": 2",
          "}"
        ]
    )

spec :: Spec
spec =
  describe "Clear Scroll API response" $ do
    it "decodes a well-formed {succeeded,num_freed} body" $ do
      let decoded = decode sampleClearScrollResponseBody :: Maybe ClearScrollResponse
      decoded `shouldSatisfy` isJust
      let Just r = decoded
      clearScrollSucceeded r `shouldBe` True
      clearScrollNumFreed r `shouldBe` 2

    it "round-trips decode -> encode -> decode" $ do
      let Just (r1 :: ClearScrollResponse) = decode sampleClearScrollResponseBody
      let reencoded = encode r1
      decode reencoded `shouldBe` Just r1

    it "rejects a body missing num_freed" $ do
      let malformed =
            LBS.pack
              ( unlines
                  [ "{",
                    "  \"succeeded\": true",
                    "}"
                  ]
              )
      let result = parseEither parseJSON =<< eitherDecode malformed :: Either String ClearScrollResponse
      result `shouldSatisfy` isLeft

    it "rejects a body missing succeeded" $ do
      let malformed =
            LBS.pack
              ( unlines
                  [ "{",
                    "  \"num_freed\": 1",
                    "}"
                  ]
              )
      let result = parseEither parseJSON =<< eitherDecode malformed :: Either String ClearScrollResponse
      result `shouldSatisfy` isLeft

    it "rejects a non-object body" $ do
      let result = parseEither parseJSON =<< eitherDecode "[1,2,3]" :: Either String ClearScrollResponse
      result `shouldSatisfy` isLeft

    describe "advanceScrollOptionsParams rendering" $ do
      -- The renderer is the wire contract for the new @?rest_total_hits_as_int@
      -- parameter on @POST /_search/scroll@. These tests pin its output so
      -- regressions are caught even without an integration test.
      it "defaultAdvanceScrollOptions renders no parameters" $ do
        advanceScrollOptionsParams defaultAdvanceScrollOptions
          `shouldBe` []

      it "asoRestTotalHitsAsInt = Just True renders rest_total_hits_as_int=true" $ do
        let opts = defaultAdvanceScrollOptions {asoRestTotalHitsAsInt = Just True}
        advanceScrollOptionsParams opts
          `shouldBe` [("rest_total_hits_as_int", Just "true")]

      it "asoRestTotalHitsAsInt = Just False renders rest_total_hits_as_int=false" $ do
        let opts = defaultAdvanceScrollOptions {asoRestTotalHitsAsInt = Just False}
        advanceScrollOptionsParams opts
          `shouldBe` [("rest_total_hits_as_int", Just "false")]

    describe "HitsTotal parses both wire shapes" $ do
      -- The server emits the object form by default and the bare-integer
      -- form when the client passes @?rest_total_hits_as_int=true@. Both
      -- must decode into the same 'HitsTotal' value for the option to be
      -- usable.
      it "decodes the object form {value, relation}" $ do
        let decoded = decode "{ \"value\": 5, \"relation\": \"eq\" }" :: Maybe HitsTotal
        decoded `shouldBe` Just (HitsTotal 5 HTR_EQ)

      it "decodes a bare integer as value with relation=eq" $ do
        let decoded = decode "5" :: Maybe HitsTotal
        decoded `shouldBe` Just (HitsTotal 5 HTR_EQ)

      it "rejects a non-numeric, non-object scalar" $ do
        let result = parseEither parseJSON =<< eitherDecode "\"five\"" :: Either String HitsTotal
        result `shouldSatisfy` isLeft