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