packages feed

bloodhound-1.0.0.0: tests/Test/ValidateSpec.hs

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

module Test.ValidateSpec (spec) where

import Data.List (sort)
import Database.Bloodhound.Common.Types
import TestsUtils.Common
import TestsUtils.Import

-- | Stable ordering for equality: 'withQueries' preserves the order in
-- which params are emitted, but the tests compare the @Set@ of params
-- to avoid coupling to internal field ordering of 'ValidateOptions'.
normalize :: [(Text, Maybe Text)] -> [(Text, Maybe Text)]
normalize = sort

spec :: Spec
spec = do
  -- ------------------------------------------------------------------ --
  -- Pure JSON decoder tests (no ES required).                          --
  -- ------------------------------------------------------------------ --
  describe "ValidateQueryResponse JSON parsing" $ do
    let minimalBody = "{\"valid\":true}"
        withShardsBody =
          "{\"valid\":true,\
          \\"_shards\":{\"total\":2,\"successful\":2,\"skipped\":0,\"failed\":0}}"
        withExplanationsBody =
          "{\"valid\":false,\
          \\"_shards\":{\"total\":1,\"successful\":1,\"skipped\":0,\"failed\":0},\
          \\"explanations\":[{\"index\":\"my-idx-000001\",\"valid\":false,\
          \\"error\":\"failed to parse query [foo]\"}]}"

    it "parses the minimal {\"valid\":bool} response" $ do
      let Just (r :: ValidateQueryResponse) = decode minimalBody
      validateQueryResponseValid r `shouldBe` True
      validateQueryResponseShards r `shouldBe` Nothing
      validateQueryResponseExplanations r `shouldBe` Nothing

    it "parses _shards when present (all=true form)" $ do
      let Just (r :: ValidateQueryResponse) = decode withShardsBody
      validateQueryResponseValid r `shouldBe` True
      case validateQueryResponseShards r of
        Just cs -> do
          csTotal cs `shouldBe` 2
          csSuccessful cs `shouldBe` 2
          csSkipped cs `shouldBe` 0
          csFailed cs `shouldBe` 0
        Nothing -> expectationFailure "expected _shards object, got Nothing"
      validateQueryResponseExplanations r `shouldBe` Nothing

    it "parses explanations when present (explain=true form)" $ do
      let Just (r :: ValidateQueryResponse) = decode withExplanationsBody
      validateQueryResponseValid r `shouldBe` False
      case validateQueryResponseExplanations r of
        Just [e] -> do
          validateExplanationIndex e `shouldBe` Just [qqIndexName|my-idx-000001|]
          validateExplanationValid e `shouldBe` False
          validateExplanationError e `shouldBe` Just "failed to parse query [foo]"
          validateExplanationExplanation e `shouldBe` Nothing
        other ->
          expectationFailure ("expected singleton explanations list, got " <> show other)

    it "round-trips the full response via ToJSON/FromJSON" $ do
      let Just (original :: ValidateQueryResponse) = decode withExplanationsBody
          reDecoded = decode (encode original) :: Maybe ValidateQueryResponse
      Just original `shouldBe` reDecoded

  describe "ValidateExplanation JSON parsing" $ do
    let fullBody =
          "{\"index\":\"my-idx-000001\",\"shard\":\"0\",\"valid\":true,\
          \\"explanation\":\"+*:* #*:*\"}"
        minimalBody = "{\"valid\":true}"

    it "parses all fields when present" $ do
      let Just (e :: ValidateExplanation) = decode fullBody
      validateExplanationIndex e `shouldBe` Just [qqIndexName|my-idx-000001|]
      validateExplanationShard e `shouldBe` Just "0"
      validateExplanationValid e `shouldBe` True
      validateExplanationExplanation e `shouldBe` Just "+*:* #*:*"
      validateExplanationError e `shouldBe` Nothing

    it "parses the minimal {\"valid\":bool} form" $ do
      let Just (e :: ValidateExplanation) = decode minimalBody
      validateExplanationValid e `shouldBe` True
      validateExplanationIndex e `shouldBe` Nothing

    it "uses parseJSON directly (covers the withObject path)" $
      case parseEither parseJSON (object ["valid" .= True]) :: Either String ValidateExplanation of
        Right e -> validateExplanationValid e `shouldBe` True
        Left err -> expectationFailure ("parseJSON failed: " <> err)

  -- ------------------------------------------------------------------ --
  -- URI param rendering (no ES required).                              --
  -- ------------------------------------------------------------------ --
  describe "ValidateOptions URI param rendering" $ do
    it "defaultValidateOptions emits no params" $
      validateOptionsParams defaultValidateOptions `shouldBe` []

    it "renders explain and all as true/false strings" $ do
      let opts =
            defaultValidateOptions
              { voExplain = Just True,
                voAll = Just False
              }
      normalize (validateOptionsParams opts)
        `shouldBe` [ ("all", Just "false"),
                     ("explain", Just "true")
                   ]

    it "renders expand_wildcards as a comma-separated list" $ do
      let opts =
            defaultValidateOptions
              { voExpandWildcards = Just (ExpandWildcardsOpen :| [ExpandWildcardsClosed])
              }
      validateOptionsParams opts
        `shouldBe` [("expand_wildcards", Just "open,closed")]

    it "renders default_operator in canonical uppercase form" $ do
      let optsAnd = defaultValidateOptions {voDefaultOperator = Just And}
          optsOr = defaultValidateOptions {voDefaultOperator = Just Or}
      normalize (validateOptionsParams optsAnd) `shouldBe` [("default_operator", Just "AND")]
      normalize (validateOptionsParams optsOr) `shouldBe` [("default_operator", Just "OR")]

    it "emits the full param set when every field is populated" $ do
      let opts =
            defaultValidateOptions
              { voExplain = Just True,
                voAll = Just True,
                voRewrite = Just "constant_score_boolean",
                voIgnoreUnavailable = Just True,
                voAllowNoIndices = Just True,
                voExpandWildcards = Just (ExpandWildcardsOpen :| []),
                voQ = Just "foo:bar",
                voAnalyzer = Just "standard",
                voAnalyzeWildcard = Just True,
                voDefaultOperator = Just And,
                voDf = Just "foo",
                voLenient = Just True,
                voDefaultField = Just "foo"
              }
      let rendered = validateOptionsParams opts
      length rendered `shouldBe` 13
      map snd rendered `shouldSatisfy` all isJust
      sort (map fst rendered)
        `shouldBe` sort
          [ "allow_no_indices",
            "all",
            "analyze_wildcard",
            "analyzer",
            "default_field",
            "default_operator",
            "df",
            "expand_wildcards",
            "explain",
            "ignore_unavailable",
            "lenient",
            "q",
            "rewrite"
          ]

  -- ------------------------------------------------------------------ --
  -- Endpoint shape: pure checks against the BHRequest value; no live   --
  -- backend needed. Mirrors the putILMPolicy / createSnapshot shape     --
  -- tests.                                                              --
  -- ------------------------------------------------------------------ --
  describe "validateQuery endpoint shape" $ do
    let idx = [qqIndexName|my-idx|]
        q = ValidateQuery (MatchAllQuery Nothing)

    it "POSTs /{index}/_validate/query with no params by default" $ do
      let req = validateQuery idx q
      bhRequestMethod req `shouldBe` "POST"
      getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["my-idx", "_validate", "query"]
      getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []

    it "wraps the Query as {\"query\":...} in the body" $ do
      let req = validateQuery idx q
      bhRequestBody req `shouldSatisfy` isJust

    it "forwards explain=true as a query param" $ do
      let opts = defaultValidateOptions {voExplain = Just True}
          req = validateQueryWith (Just [idx]) opts q
      lookup "explain" (getRawEndpointQueries (bhRequestEndpoint req))
        `shouldBe` Just (Just "true")

    it "builds /_validate/query (no index segment) for Nothing / Just []" $ do
      let reqNothing = validateQueryWith Nothing defaultValidateOptions q
          reqEmpty = validateQueryWith (Just []) defaultValidateOptions q
      getRawEndpoint (bhRequestEndpoint reqNothing) `shouldBe` ["_validate", "query"]
      getRawEndpoint (bhRequestEndpoint reqEmpty) `shouldBe` ["_validate", "query"]

    it "comma-joins multiple indices" $ do
      let idx2 = [qqIndexName|other-idx|]
          req = validateQueryWith (Just [idx, idx2]) defaultValidateOptions q
      getRawEndpoint (bhRequestEndpoint req)
        `shouldBe` ["my-idx,other-idx", "_validate", "query"]

  -- ------------------------------------------------------------------ --
  -- Live backend tests (need ES on ES_TEST_SERVER). The pure tests      --
  -- above are the must-haves; these exercise the wire round-trip.       --
  -- ------------------------------------------------------------------ --
  describe "validateQuery live API" $ do
    it "validateQuery accepts a match_all on the test index (valid:true)" $
      withTestEnv $ do
        _ <- insertData
        r <-
          performBHRequest $
            validateQuery testIndex (ValidateQuery (MatchAllQuery Nothing))
        liftIO $ validateQueryResponseValid r `shouldBe` True

    it "validateAll matches validateQuery against the test index" $
      withTestEnv $ do
        _ <- insertData
        let q = ValidateQuery (MatchAllQuery Nothing)
        viaAll <- performBHRequest $ validateAll q
        viaIdx <- performBHRequest $ validateQuery testIndex q
        liftIO $ do
          validateQueryResponseValid viaAll
            `shouldBe` validateQueryResponseValid viaIdx
          -- validateAll hits POST /_validate/query (no index path),
          -- which is the form that surfaces the @_shards@ summary.
          validateQueryResponseShards viaAll `shouldSatisfy` (\case Just _ -> True; Nothing -> False)

    it "validateQueryWith explain=true surfaces a non-empty explanations array" $
      withTestEnv $ do
        _ <- insertData
        r <-
          performBHRequest $
            validateQueryWith
              (Just [testIndex])
              defaultValidateOptions {voExplain = Just True}
              (ValidateQuery (MatchAllQuery Nothing))
        liftIO $
          validateQueryResponseExplanations r `shouldSatisfy` \case
            Just xs -> not (null xs)
            Nothing -> False