packages feed

bloodhound-1.0.0.0: tests/Test/DownsampleSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.DownsampleSpec (spec) where

import Data.Aeson
import Data.ByteString.Lazy.Char8 qualified as LBS
import Database.Bloodhound.ElasticSearch9.Requests qualified as RequestsES9
import Database.Bloodhound.ElasticSearch9.Types qualified as Types
import TestsUtils.Import

spec :: Spec
spec = describe "Downsample index API (/{index}/_downsample/{target_index})" $ do
  describe "DownsampleRequest JSON" $ do
    it "encodes a minimal request (no sampling_method)" $ do
      let req = Types.DownsampleRequest (FixedInterval 1 Hours) Nothing
      encode req `shouldBe` "{\"fixed_interval\":\"1h\"}"

    it "round-trips a minimal request" $ do
      let req = Types.DownsampleRequest (FixedInterval 1 Hours) Nothing
      decode (encode req) `shouldBe` Just req

    it "encodes sampling_method: aggregate as a JSON string" $ do
      let req =
            Types.DownsampleRequest
              (FixedInterval 1 Hours)
              (Just Types.Aggregate)
      -- Inspect the wire form (order-independent via Object equality):
      -- \"aggregate\" must be a string.
      decode (encode req)
        `shouldBe` Just
          ( object
              [ "fixed_interval" .= String "1h",
                "sampling_method" .= String "aggregate"
              ]
          )

    it "encodes sampling_method: last_value as a JSON string" $ do
      let req =
            Types.DownsampleRequest
              (FixedInterval 30 Minutes)
              (Just Types.LastValue)
      decode (encode req)
        `shouldBe` Just
          ( object
              [ "fixed_interval" .= String "30m",
                "sampling_method" .= String "last_value"
              ]
          )

    it "round-trips a request with sampling_method" $ do
      let req =
            Types.DownsampleRequest
              (FixedInterval 30 Minutes)
              (Just Types.LastValue)
      decode (encode req) `shouldBe` Just req

    it "decodes a fully-populated request body" $ do
      let raw =
            LBS.pack
              "{\"fixed_interval\":\"1h\",\"sampling_method\":\"last_value\"}"
      case decode raw :: Maybe Types.DownsampleRequest of
        Just req -> do
          Types.downsampleRequestSamplingMethod req
            `shouldBe` Just Types.LastValue
        Nothing ->
          expectationFailure "failed to decode DownsampleRequest"

    it "decodes a body without sampling_method (server default aggregate)" $ do
      let raw = LBS.pack "{\"fixed_interval\":\"1h\"}"
      case decode raw :: Maybe Types.DownsampleRequest of
        Just req ->
          Types.downsampleRequestSamplingMethod req
            `shouldBe` (Nothing :: Maybe Types.SamplingMethod)
        Nothing ->
          expectationFailure "failed to decode DownsampleRequest"

    it "rejects an unknown sampling_method value" $ do
      let raw = LBS.pack "{\"fixed_interval\":\"1h\",\"sampling_method\":\"bogus\"}"
      decode raw `shouldBe` (Nothing :: Maybe Types.DownsampleRequest)

  describe "DownsampleResponse JSON" $ do
    it "round-trips an acknowledged response" $ do
      let resp = Types.DownsampleResponse True True
      decode (encode resp) `shouldBe` Just resp

    it "decodes {acknowledged, shards_acknowledged} both true" $
      decode (LBS.pack "{\"acknowledged\":true,\"shards_acknowledged\":true}")
        `shouldBe` Just (Types.DownsampleResponse True True)

    it "defaults shards_acknowledged to False when absent" $
      decode (LBS.pack "{\"acknowledged\":true}")
        `shouldBe` Just (Types.DownsampleResponse True False)

    it "decodes an empty object as both False" $
      decode (LBS.pack "{}")
        `shouldBe` Just (Types.DownsampleResponse False False)

  describe "downsampleOptionsParams URI rendering" $ do
    it "defaultDownsampleOptions emits no params" $
      Types.downsampleOptionsParams Types.defaultDownsampleOptions
        `shouldBe` []

    it "renders wait_for_active_shards: all as \"all\"" $ do
      let opts =
            Types.defaultDownsampleOptions
              { Types.downsampleOptionsWaitForActiveShards = Just AllActiveShards
              }
      Types.downsampleOptionsParams opts
        `shouldBe` [("wait_for_active_shards", Just "all")]

    it "renders wait_for_active_shards: n as a bare number" $ do
      let opts =
            Types.defaultDownsampleOptions
              { Types.downsampleOptionsWaitForActiveShards = Just (ActiveShards 2)
              }
      Types.downsampleOptionsParams opts
        `shouldBe` [("wait_for_active_shards", Just "2")]

    it "renders master_timeout and timeout" $ do
      let opts =
            Types.defaultDownsampleOptions
              { Types.downsampleOptionsMasterTimeout = Just "30s",
                Types.downsampleOptionsTimeout = Just "1m"
              }
      Types.downsampleOptionsParams opts
        `shouldBe` [("master_timeout", Just "30s"), ("timeout", Just "1m")]

    it "renders only the set field" $ do
      let opts =
            Types.defaultDownsampleOptions
              { Types.downsampleOptionsTimeout = Just "5s"
              }
      Types.downsampleOptionsParams opts
        `shouldBe` [("timeout", Just "5s")]

    it "renders all three params together in canonical order" $ do
      let opts =
            Types.defaultDownsampleOptions
              { Types.downsampleOptionsWaitForActiveShards = Just AllActiveShards,
                Types.downsampleOptionsMasterTimeout = Just "10s",
                Types.downsampleOptionsTimeout = Just "1m"
              }
      Types.downsampleOptionsParams opts
        `shouldBe` [ ("wait_for_active_shards", Just "all"),
                     ("master_timeout", Just "10s"),
                     ("timeout", Just "1m")
                   ]

  describe "endpoint shape" $ do
    let body = Types.DownsampleRequest (FixedInterval 1 Hours) Nothing
        Right idx = mkIndexName "metrics"
        Right target = mkIndexName "metrics-downsample"

    it "POSTs /<index>/_downsample/<target_index> with a body" $ do
      let req = RequestsES9.downsampleIndex idx target body
      getRawEndpoint (bhRequestEndpoint req)
        `shouldBe` ["metrics", "_downsample", "metrics-downsample"]
      bhRequestBody req `shouldSatisfy` isJust

    it "downsampleIndexWith appends timeout query params" $ do
      let opts =
            Types.defaultDownsampleOptions
              { Types.downsampleOptionsMasterTimeout = Just "10s"
              }
          req = RequestsES9.downsampleIndexWith idx target body opts
      getRawEndpoint (bhRequestEndpoint req)
        `shouldBe` ["metrics", "_downsample", "metrics-downsample"]
      getRawEndpointQueries (bhRequestEndpoint req)
        `shouldBe` [("master_timeout", Just "10s")]

    it "downsampleIndexWith forwards wait_for_active_shards as a query param" $ do
      let opts =
            Types.defaultDownsampleOptions
              { Types.downsampleOptionsWaitForActiveShards = Just AllActiveShards
              }
          req = RequestsES9.downsampleIndexWith idx target body opts
      getRawEndpoint (bhRequestEndpoint req)
        `shouldBe` ["metrics", "_downsample", "metrics-downsample"]
      getRawEndpointQueries (bhRequestEndpoint req)
        `shouldBe` [("wait_for_active_shards", Just "all")]

    it "the request body round-trips through the DownsampleRequest type" $ do
      let req = RequestsES9.downsampleIndex idx target body
      case bhRequestBody req of
        Just bs -> case decode bs :: Maybe Types.DownsampleRequest of
          Just decoded ->
            decoded `shouldBe` body
          Nothing ->
            expectationFailure "request body did not decode as DownsampleRequest"
        Nothing ->
          expectationFailure "downsampleIndex should carry a request body"