packages feed

bloodhound-1.0.0.0: tests/Test/TextStructureSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.TextStructureSpec (spec) where

import Data.Aeson (decode, encode)
import Data.ByteString.Lazy.Char8 qualified as LBS
import Data.List (sort)
import Database.Bloodhound.Client.Cluster (BackendType (..))
import Database.Bloodhound.ElasticSearch8.Client qualified as ClientES8
import Database.Bloodhound.ElasticSearch8.Requests qualified as RequestsES8
import Database.Bloodhound.ElasticSearch8.Types qualified as Types
import TestsUtils.Common
import TestsUtils.Import

normalize :: [(Text, Maybe Text)] -> [(Text, Maybe Text)]
normalize = sort

spec :: Spec
spec =
  describe "Text structure API (find_message_structure, find_field_structure, test_grok_pattern)" $ do
    describe "FindMessageStructureRequest JSON" $ do
      it "encodes as {messages: [...]}" $ do
        let req = Types.FindMessageStructureRequest ["line one", "line two"]
        encode req
          `shouldBe` "{\"messages\":[\"line one\",\"line two\"]}"
        decode (encode req) `shouldBe` Just req

      it "encodes an empty message list as {messages: []}" $
        encode (Types.FindMessageStructureRequest [])
          `shouldBe` "{\"messages\":[]}"

      it "decodes a response missing the messages key as []" $
        decode "{}" `shouldBe` Just (Types.FindMessageStructureRequest [])

    describe "TestGrokPatternRequest JSON" $ do
      it "encodes as {grok_pattern, text}" $ do
        let req =
              Types.TestGrokPatternRequest
                "%{IP:ip}"
                ["127.0.0.1"]
        encode req
          `shouldBe` "{\"grok_pattern\":\"%{IP:ip}\",\"text\":[\"127.0.0.1\"]}"
        decode (encode req) `shouldBe` Just req

      it "decodes a body missing the text key as []" $ do
        let raw = LBS.pack "{\"grok_pattern\":\"%{IP:ip}\"}"
        decode raw
          `shouldBe` Just (Types.TestGrokPatternRequest "%{IP:ip}" [])

    describe "TestGrokPatternResponse JSON" $ do
      it "decodes a populated matches array" $ do
        let raw =
              LBS.pack
                "{\"matches\":[{\"ip\":\"127.0.0.1\"},{}]}"
        case decode raw :: Maybe Types.TestGrokPatternResponse of
          Just resp -> length (Types.tgprMatches resp) `shouldBe` 2
          Nothing -> expectationFailure "failed to decode TestGrokPatternResponse"

      it "decodes a missing matches key as []" $
        decode "{}"
          `shouldBe` Just (Types.TestGrokPatternResponse [])

      it "round-trips a populated response" $ do
        let resp = Types.TestGrokPatternResponse {Types.tgprMatches = []}
        decode (encode resp) `shouldBe` Just resp

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

      it "renders the message-classifier overrides" $ do
        let opts =
              Types.defaultFindMessageStructureOptions
                { Types.fmsoFormat = Just "ndjson",
                  Types.fmsoExplain = Just True,
                  Types.fmsoEcsCompatibility = Just "v1"
                }
        normalize (Types.findMessageStructureOptionsParams opts)
          `shouldBe` sort
            [ ("ecs_compatibility", Just "v1"),
              ("explain", Just "true"),
              ("format", Just "ndjson")
            ]

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

      it "renders the required index/field plus overrides" $ do
        let opts =
              Types.defaultFindFieldStructureOptions
                { Types.ffsoIndex = Just "logs",
                  Types.ffsoField = Just "message",
                  Types.ffsoDocumentsToSample = Just 1000,
                  Types.ffsoExplain = Just True
                }
        normalize (Types.findFieldStructureOptionsParams opts)
          `shouldBe` sort
            [ ("documents_to_sample", Just "1000"),
              ("explain", Just "true"),
              ("field", Just "message"),
              ("index", Just "logs")
            ]

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

      it "renders ecs_compatibility" $
        Types.testGrokPatternOptionsParams
          Types.defaultTestGrokPatternOptions
            { Types.tgpoEcsCompatibility = Just "v1"
            }
          `shouldBe` [("ecs_compatibility", Just "v1")]

    describe "endpoint shape" $ do
      it "POSTs /_text_structure/find_message_structure with a JSON body" $ do
        let req =
              RequestsES8.findMessageStructure
                (Types.FindMessageStructureRequest ["a", "b"])
        getRawEndpoint (bhRequestEndpoint req)
          `shouldBe` ["_text_structure", "find_message_structure"]
        bhRequestBody req `shouldSatisfy` isJust

      it "findMessageStructureWith appends query params" $ do
        let opts =
              Types.defaultFindMessageStructureOptions
                { Types.fmsoFormat = Just "ndjson"
                }
            req =
              RequestsES8.findMessageStructureWith
                opts
                (Types.FindMessageStructureRequest ["a"])
        getRawEndpoint (bhRequestEndpoint req)
          `shouldBe` ["_text_structure", "find_message_structure"]
        getRawEndpointQueries (bhRequestEndpoint req)
          `shouldBe` [("format", Just "ndjson")]

      it "GETs /_text_structure/find_field_structure with no body" $ do
        let req = RequestsES8.findFieldStructure "logs" "message"
        getRawEndpoint (bhRequestEndpoint req)
          `shouldBe` ["_text_structure", "find_field_structure"]
        bhRequestBody req `shouldSatisfy` isNothing
        normalize (getRawEndpointQueries (bhRequestEndpoint req))
          `shouldBe` sort [("field", Just "message"), ("index", Just "logs")]

      it "findFieldStructureWith forwards extra overrides" $ do
        let opts =
              Types.defaultFindFieldStructureOptions
                { Types.ffsoIndex = Just "logs",
                  Types.ffsoField = Just "message",
                  Types.ffsoDocumentsToSample = Just 500
                }
            req = RequestsES8.findFieldStructureWith opts
        getRawEndpoint (bhRequestEndpoint req)
          `shouldBe` ["_text_structure", "find_field_structure"]
        bhRequestBody req `shouldSatisfy` isNothing
        normalize (getRawEndpointQueries (bhRequestEndpoint req))
          `shouldBe` sort
            [ ("documents_to_sample", Just "500"),
              ("field", Just "message"),
              ("index", Just "logs")
            ]

      it "POSTs /_text_structure/test_grok_pattern with a JSON body" $ do
        let req =
              RequestsES8.testGrokPattern
                (Types.TestGrokPatternRequest "%{IP:ip}" ["1.2.3.4"])
        getRawEndpoint (bhRequestEndpoint req)
          `shouldBe` ["_text_structure", "test_grok_pattern"]
        bhRequestBody req `shouldSatisfy` isJust

      it "testGrokPatternWith appends ecs_compatibility" $ do
        let req =
              RequestsES8.testGrokPatternWith
                Types.defaultTestGrokPatternOptions
                  { Types.tgpoEcsCompatibility = Just "v1"
                  }
                (Types.TestGrokPatternRequest "%{IP:ip}" ["1.2.3.4"])
        getRawEndpoint (bhRequestEndpoint req)
          `shouldBe` ["_text_structure", "test_grok_pattern"]
        getRawEndpointQueries (bhRequestEndpoint req)
          `shouldBe` [("ecs_compatibility", Just "v1")]

    describe "live integration (requires ES8+ backend)" $
      backendSpecific [ElasticSearch8] $ do
        it "test_grok_pattern classifies a simple IP pattern" $
          withTestEnv $ do
            resp <-
              ClientES8.testGrokPattern
                (Types.TestGrokPatternRequest "%{IP:ip}" ["127.0.0.1"])
            liftIO $ length (Types.tgprMatches resp) `shouldBe` 1