packages feed

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")