bloodhound-1.0.0.0: tests/Test/ParseEsResponseSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module Test.ParseEsResponseSpec (spec) where
import Data.Aeson (decode, eitherDecode)
import TestsUtils.Import
import Prelude
-- | Pure unit tests for the empty-body-2xx handling fixed in bead
-- @bloodhound-vvj@. ES 7.17 returns HTTP 200 with Content-Length: 0
-- for several endpoints typed as 'Acknowledged';
-- 'parseEsResponse' normalises such an empty body to the JSON value
-- @null@ before invoking the 'FromJSON' instance, and the
-- 'Acknowledged' 'FromJSON' instance maps @null@ to 'Acknowledged'
-- @True@. These tests pin the second half of that mapping (the
-- 'Acknowledged' 'FromJSON' change). The 'parseEsResponse'
-- empty-body normalisation itself is covered end-to-end by the live
-- round-trip in 'Test.VotingConfigExclusionsSpec' (gated to ES
-- backends). Constructing a 'BHResponse' purely would require
-- reaching into http-client internals (its 'Response' constructor is
-- not exported from the public module), so the integration coverage
-- is left to the live test rather than duplicated here.
--
-- Note: the OS k-NN\/Neural endpoints (covered by
-- 'Test.KnnWarmupSpec', 'Test.KnnClearCacheSpec',
-- 'Test.NeuralWarmupSpec', 'Test.NeuralClearCacheSpec') were
-- /suspected/ to need this same handling but turn out not to — see
-- their @pendingWith@ reasons for the separate OS library bugs
-- (wrong HTTP method, wrong response type) that block their live
-- round-trips. They do not exercise this normalisation.
spec :: Spec
spec = do
describe "Acknowledged FromJSON null-handling" $ do
it "decodes the literal JSON null as Acknowledged True" $ do
decode "null" `shouldBe` (Just (Acknowledged True))
it "still decodes {\"acknowledged\": true} as Acknowledged True" $ do
decode "{\"acknowledged\": true}" `shouldBe` (Just (Acknowledged True))
it "still decodes {\"acknowledged\": false} as Acknowledged False" $ do
decode "{\"acknowledged\": false}" `shouldBe` (Just (Acknowledged False))
it "still rejects a missing acknowledged key" $ do
decode "{}" `shouldBe` (Nothing :: Maybe Acknowledged)
it "rejects non-object, non-null shapes (booleans, numbers, arrays)" $ do
decode "true" `shouldBe` (Nothing :: Maybe Acknowledged)
decode "42" `shouldBe` (Nothing :: Maybe Acknowledged)
decode "[]" `shouldBe` (Nothing :: Maybe Acknowledged)
describe "Acknowledged round-trip with the empty-body normalisation" $ do
-- 'parseEsResponse' normalises an empty 2xx body to the literal
-- JSON @null@ before decoding. Pin the decode of that normalised
-- payload here so a regression in either half of the fix is
-- surfaced in this spec.
it "eitherDecode \"null\" succeeds for Acknowledged (mirrors parseEsResponse's normalisation)" $ do
eitherDecode "null" `shouldBe` (Right (Acknowledged True) :: Either String Acknowledged)
-- Regression coverage for the 'EsError' 'FromJSON' instance. The OS
-- 1.3 server bug on @GET /_script_language@ returns HTTP 500 with a
-- body of
-- @{"error":{"root_cause":[{"type":"null_pointer_exception","reason":null}],"type":"null_pointer_exception","reason":null},"status":500}@.
-- The historical parser required @error.reason@ to be a non-null
-- 'Text' and so rejected this body, which left 'parseEsResponse'
-- unable to surface the failure as an 'EsError' and instead threw
-- 'EsProtocolException'. The instance is now null-tolerant: it
-- falls back to @error.type@, then a placeholder, and finally to a
-- catch-all parser so any future/unknown shape still yields an
-- 'EsError' rather than a protocol exception.
describe "EsError FromJSON null-tolerant reason" $ do
it "falls back to error.type when error.reason is null" $ do
decode
"{\"error\":{\"type\":\"null_pointer_exception\",\"reason\":null},\"status\":500}"
`shouldBe` Just (EsError (Just 500) "null_pointer_exception")
it "uses error.reason when it is a string" $ do
decode
"{\"error\":{\"type\":\"some_type\",\"reason\":\"boom\"},\"status\":400}"
`shouldBe` Just (EsError (Just 400) "boom")
it "falls back to error.type when error.reason is missing" $ do
decode
"{\"error\":{\"type\":\"illegal_argument_exception\"},\"status\":400}"
`shouldBe` Just (EsError (Just 400) "illegal_argument_exception")
it "uses placeholder when neither reason nor type is present" $ do
decode "{\"error\":{},\"status\":500}"
`shouldBe` Just (EsError (Just 500) "unknown error")
it "still parses the bare-string error shape" $ do
decode "{\"error\":\"something went wrong\",\"status\":500}"
`shouldBe` Just (EsError (Just 500) "something went wrong")
it "falls back to cause.type when failures[].cause.reason is null" $ do
decode
"{\"failures\":[{\"status\":400,\"cause\":{\"type\":\"x\",\"reason\":null}}]}"
`shouldBe` Just (EsError (Just 400) "x")
it "uses cause.reason when it is a string in the failures-array shape" $ do
decode
"{\"failures\":[{\"status\":422,\"cause\":{\"type\":\"y\",\"reason\":\"nope\"}}]}"
`shouldBe` Just (EsError (Just 422) "nope")
it "catch-all surfaces unrecognised shapes as EsError rather than failing" $ do
decode "{\"weird\":\"shape\",\"status\":418}"
`shouldBe` Just (EsError (Just 418) "unrecognised error body")