packages feed

bloodhound-1.0.0.0: tests/Test/RenderTemplateSpec.hs

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

module Test.RenderTemplateSpec (spec) where

import Data.Aeson qualified as Aeson
import Data.Aeson.KeyMap qualified as KM
import TestsUtils.Import
import Prelude

-- | Pure endpoint-shape tests for the render-template endpoint
-- (@GET /_render/template@ and @GET /_render/template\/{id}@). No live
-- cluster round-trip — the live integration tests live next to the
-- existing search-template tests in 'Test.QuerySpec'. These pin the
-- wire contract: GET method (not POST, unlike every sibling
-- @*_search/template@ endpoint), GET-with-body (unusual for ES but
-- required here), path derived from the 'searchTemplate' field, and
-- the encoded 'SearchTemplate' carried verbatim as the body.
spec :: Spec
spec = describe "render template endpoint shape" $ do
  let src =
        SearchTemplateSource
          "{\"query\":{\"match\":{\"{{field}}\":\"{{value}}\"}}}"
      params =
        TemplateQueryKeyValuePairs $
          KM.fromList
            [ ("field", "user"),
              ("value", "bitemyapp")
            ]
      inlineSearch = mkSearchTemplate (Right src) params
      tid = SearchTemplateId "my-rendered-template"
      storedSearch = mkSearchTemplate (Left tid) params

  describe "GET /_render/template (inline source)" $ do
    it "uses the GET method (not POST like sibling _search/template)" $ do
      bhRequestMethod (renderTemplate inlineSearch) `shouldBe` "GET"

    it "targets /_render/template with no trailing id segment" $ do
      let req = renderTemplate inlineSearch
      getRawEndpoint (bhRequestEndpoint req)
        `shouldBe` ["_render", "template"]

    it "carries no query string" $ do
      let req = renderTemplate inlineSearch
      getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []

    it "attaches the encoded SearchTemplate as a body (GET-with-body)" $ do
      let req = renderTemplate inlineSearch
      bhRequestBody req `shouldSatisfy` isJust

    it "the body contains the inline source under the \"source\" key" $ do
      let req = renderTemplate inlineSearch
      bodyHasKey req "source" `shouldBe` True

    it "the body carries the params under the \"params\" key" $ do
      let req = renderTemplate inlineSearch
      bodyHasKey req "params" `shouldBe` True

  describe "GET /_render/template/{id} (stored template)" $ do
    it "uses the GET method" $ do
      bhRequestMethod (renderTemplate storedSearch) `shouldBe` "GET"

    it "targets /_render/template/{id} lifting the SearchTemplateId into the path" $ do
      let req = renderTemplate storedSearch
      getRawEndpoint (bhRequestEndpoint req)
        `shouldBe` ["_render", "template", "my-rendered-template"]

    it "uses a distinct id in the path when given a different SearchTemplateId" $ do
      let otherSearch =
            mkSearchTemplate
              (Left (SearchTemplateId "a-different-id"))
              params
          req = renderTemplate otherSearch
      getRawEndpoint (bhRequestEndpoint req)
        `shouldBe` ["_render", "template", "a-different-id"]

    it "keeps the id as a single opaque path segment" $ do
      -- Client-side invariant only: the builder does not split an id
      -- containing characters that look like path separators. A real
      -- ES/OpenSearch server will reject ids containing @/@ as
      -- malformed path segments — this test pins the *client's*
      -- no-splitting behaviour, not server-side acceptance.
      let weirdSearch =
            mkSearchTemplate
              (Left (SearchTemplateId "weird_42/id"))
              params
          req = renderTemplate weirdSearch
      getRawEndpoint (bhRequestEndpoint req)
        `shouldBe` ["_render", "template", "weird_42/id"]

    it "carries no query string" $ do
      let req = renderTemplate storedSearch
      getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []

    it "attaches the encoded SearchTemplate as a body (GET-with-body)" $ do
      let req = renderTemplate storedSearch
      bhRequestBody req `shouldSatisfy` isJust

    it "the body contains the id under the \"id\" key (the path wins server-side; the body copy is harmless)" $ do
      -- The server takes the id from the path segment and ignores the
      -- body copy; we send the SearchTemplate verbatim rather than
      -- surgically stripping the id, so this asserts the simpler
      -- invariant (whole SearchTemplate encoded) rather than a
      -- body-shape special-case for the {id} path variant.
      let req = renderTemplate storedSearch
      bodyHasKey req "id" `shouldBe` True

  describe "renderTemplate is distinct from searchByIndexTemplate" $ do
    -- Guards against an accidental copy-paste regression where
    -- renderTemplate silently re-uses the @/_search/template@ endpoint.
    -- The two APIs both take a 'SearchTemplate' body, but differ in
    -- method (GET vs POST), path (@_render/template@ vs
    -- @<index>/_search/template@), and semantics (render-only vs
    -- execute). The path + method are the distinguishing features.
    let indexName = [qqIndexName|test-index|]

    it "uses GET, where searchByIndexTemplate uses POST" $ do
      bhRequestMethod (renderTemplate inlineSearch)
        `shouldNotBe` bhRequestMethod (searchByIndexTemplate @Aeson.Value indexName inlineSearch)

    it "uses the _render/template path, not the _search/template path" $ do
      let renderPath = getRawEndpoint (bhRequestEndpoint (renderTemplate inlineSearch))
          searchPath = getRawEndpoint (bhRequestEndpoint (searchByIndexTemplate @Aeson.Value indexName inlineSearch))
      renderPath `shouldNotBe` searchPath

    it "the render path does not embed the IndexName (the endpoint is index-agnostic)" $ do
      let renderPath = getRawEndpoint (bhRequestEndpoint (renderTemplate inlineSearch))
      renderPath `shouldNotContain` ["test-index"]
      renderPath `shouldBe` ["_render", "template"]

  describe "renderTemplate body round-trips through SearchTemplate's ToJSON" $ do
    -- Pins the invariant that the body is exactly the encoded
    -- SearchTemplate (no envelope wrapper, no field stripping). A
    -- future change that wrapped or rewrote the body would fail here.
    it "inline body bytes equal encode of the SearchTemplate" $ do
      let req = renderTemplate inlineSearch
      bhRequestBody req `shouldBe` Just (Aeson.encode inlineSearch)

    it "stored-id body bytes equal encode of the SearchTemplate" $ do
      let req = renderTemplate storedSearch
      bhRequestBody req `shouldBe` Just (Aeson.encode storedSearch)
  where
    -- Total accessor: does the rendered request body decode to a JSON
    -- object carrying the given key? Returns 'False' (rather than
    -- throwing) when the body is absent or not a JSON object, so the
    -- failing assertion points at the invariant under test rather than
    -- at the partial pattern.
    bodyHasKey :: BHRequest StatusDependant (ParsedEsResponse Aeson.Value) -> Aeson.Key -> Bool
    bodyHasKey req key =
      case bhRequestBody req of
        Just body ->
          case Aeson.decode body :: Maybe Aeson.Value of
            Just (Aeson.Object o) -> KM.member key o
            _ -> False
        Nothing -> False