bloodhound-1.0.0.0: tests/Test/ReindexResponseSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}
module Test.ReindexResponseSpec (spec) where
import Data.Aeson (decode, encode)
import Data.ByteString.Lazy.Char8 qualified as LBS
import Data.Maybe (isJust)
import Database.Bloodhound.Common.Types
import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy)
import Test.QuickCheck (property)
import TestsUtils.Generators ()
-- | A fully-populated ES reindex response carrying the six bead-named
-- fields added in bloodhound-04f.23 (commit 1: total, deleted, noops,
-- timed_out, retries{bulk,search}, throttled_until_millis). Mirrors the
-- documented example shape.
sampleFullResponse :: LBS.ByteString
sampleFullResponse =
LBS.pack
( unlines
[ "{",
" \"took\": 3589,",
" \"updated\": 100,",
" \"created\": 200,",
" \"batches\": 1,",
" \"version_conflicts\": 0,",
" \"throttled_millis\": 0,",
" \"total\": 300,",
" \"deleted\": 0,",
" \"noops\": 0,",
" \"timed_out\": false,",
" \"retries\": {\"bulk\": 0, \"search\": 0},",
" \"throttled_until_millis\": 0",
"}"
]
)
-- | Minimal response that only carries the formerly hard-required keys.
-- Confirms the relaxed decoder still reads them and that the new fields
-- default to 'Nothing'.
sampleMinimalResponse :: LBS.ByteString
sampleMinimalResponse =
LBS.pack
( unlines
[ "{",
" \"updated\": 0,",
" \"created\": 0,",
" \"batches\": 0,",
" \"version_conflicts\": 0,",
" \"throttled_millis\": 0",
"}"
]
)
-- | The complete documented response surface (commit 2): all seventeen
-- top-level fields, a nested 'ReindexRetries', a 'ReindexFailure' with
-- a depth-2 recursive @caused_by@ chain, and two 'ReindexSliceStatus'
-- entries (one fully populated, one with just a @cancelled@ reason).
sampleCompleteResponse :: LBS.ByteString
sampleCompleteResponse =
LBS.pack
( unlines
[ "{",
" \"took\": 3589,",
" \"updated\": 100,",
" \"created\": 200,",
" \"batches\": 1,",
" \"version_conflicts\": 0,",
" \"throttled_millis\": 0,",
" \"total\": 300,",
" \"deleted\": 0,",
" \"noops\": 0,",
" \"timed_out\": false,",
" \"retries\": {\"bulk\": 0, \"search\": 0},",
" \"throttled_until_millis\": 0,",
" \"requests_per_second\": -1,",
" \"failures\": [",
" {",
" \"index\": \"src\",",
" \"id\": \"abc\",",
" \"status\": 409,",
" \"cause\": {",
" \"type\": \"version_conflict_engine_exception\",",
" \"reason\": \"[src][1]: version conflict\",",
" \"caused_by\": {",
" \"type\": \"inner_type\",",
" \"reason\": \"inner reason\"",
" }",
" }",
" }",
" ],",
" \"slices\": [",
" {",
" \"slice_id\": 0,",
" \"total\": 150,",
" \"updated\": 50,",
" \"created\": 100,",
" \"deleted\": 0,",
" \"batches\": 1,",
" \"version_conflicts\": 0,",
" \"noops\": 0,",
" \"retries\": {\"bulk\": 0, \"search\": 0},",
" \"throttled_millis\": 0,",
" \"throttled_until_millis\": 0,",
" \"requests_per_second\": -1,",
" \"throttled\": \"0s\",",
" \"throttled_until\": \"0s\"",
" },",
" {",
" \"slice_id\": 1,",
" \"cancelled\": \"user cancelled\"",
" }",
" ],",
" \"task\": \"rO0ABXN5\",",
" \"slice_id\": 0",
"}"
]
)
spec :: Spec
spec = describe "ReindexResponse JSON" $ do
it "decodes a fully-populated response" $ do
let Just decoded = decode sampleFullResponse :: Maybe ReindexResponse
reindexResponseTook decoded `shouldBe` Just 3589
reindexResponseUpdated decoded `shouldBe` Just 100
reindexResponseCreated decoded `shouldBe` Just 200
reindexResponseBatches decoded `shouldBe` Just 1
reindexResponseVersionConflicts decoded `shouldBe` Just 0
reindexResponseThrottledMillis decoded `shouldBe` Just 0
reindexResponseTotal decoded `shouldBe` Just 300
reindexResponseDeleted decoded `shouldBe` Just 0
reindexResponseNoops decoded `shouldBe` Just 0
reindexResponseTimedOut decoded `shouldBe` Just False
reindexResponseRetries decoded `shouldSatisfy` isJust
reindexResponseThrottledUntilMillis decoded `shouldBe` Just 0
it "decodes the nested retries object" $ do
let Just decoded = decode sampleFullResponse :: Maybe ReindexResponse
Just retries = reindexResponseRetries decoded
reindexRetriesBulk retries `shouldBe` Just 0
reindexRetriesSearch retries `shouldBe` Just 0
it "decodes a minimal response with new fields all Nothing" $ do
let Just decoded = decode sampleMinimalResponse :: Maybe ReindexResponse
reindexResponseTook decoded `shouldBe` Nothing
reindexResponseUpdated decoded `shouldBe` Just 0
reindexResponseCreated decoded `shouldBe` Just 0
reindexResponseBatches decoded `shouldBe` Just 0
reindexResponseVersionConflicts decoded `shouldBe` Just 0
reindexResponseThrottledMillis decoded `shouldBe` Just 0
reindexResponseTotal decoded `shouldBe` Nothing
reindexResponseDeleted decoded `shouldBe` Nothing
reindexResponseNoops decoded `shouldBe` Nothing
reindexResponseTimedOut decoded `shouldBe` Nothing
reindexResponseRetries decoded `shouldBe` Nothing
reindexResponseThrottledUntilMillis decoded `shouldBe` Nothing
it "decodes the empty object as all Nothing" $ do
let Just decoded = decode "{}" :: Maybe ReindexResponse
reindexResponseTook decoded `shouldBe` Nothing
reindexResponseUpdated decoded `shouldBe` Nothing
reindexResponseThrottledMillis decoded `shouldBe` Nothing
reindexResponseRetries decoded `shouldBe` Nothing
reindexResponseThrottledUntilMillis decoded `shouldBe` Nothing
it "round-trips a fully-populated response" $ do
let Just decoded = decode sampleFullResponse :: Maybe ReindexResponse
(decode . encode) decoded `shouldBe` Just decoded
it "round-trips a minimal response" $ do
let Just decoded = decode sampleMinimalResponse :: Maybe ReindexResponse
(decode . encode) decoded `shouldBe` Just decoded
describe "commit-2 fields on ReindexResponse" $ do
it "decodes the complete documented response surface" $ do
let Just decoded = decode sampleCompleteResponse :: Maybe ReindexResponse
reindexResponseRequestsPerSecond decoded `shouldBe` Just (-1)
reindexResponseTask decoded `shouldBe` Just "rO0ABXN5"
reindexResponseSliceId decoded `shouldBe` Just 0
-- failures: single entry with depth-2 recursive cause
reindexResponseFailures decoded `shouldSatisfy` isJust
let Just (failure : _) = reindexResponseFailures decoded
reindexFailureIndex failure `shouldBe` Just "src"
reindexFailureId failure `shouldBe` Just "abc"
reindexFailureStatus failure `shouldBe` Just 409
reindexFailureCause failure `shouldSatisfy` isJust
let Just cause = reindexFailureCause failure
reindexFailureCauseType cause `shouldBe` Just "version_conflict_engine_exception"
reindexFailureCauseReason cause `shouldBe` Just "[src][1]: version conflict"
-- recursive caused_by chain
reindexFailureCauseCausedBy cause `shouldSatisfy` isJust
let Just inner = reindexFailureCauseCausedBy cause
reindexFailureCauseType inner `shouldBe` Just "inner_type"
reindexFailureCauseReason inner `shouldBe` Just "inner reason"
reindexFailureCauseCausedBy inner `shouldBe` Nothing
-- slices: two entries, first fully populated, second cancelled
reindexResponseSlices decoded `shouldSatisfy` isJust
let Just (s0 : s1 : _) = reindexResponseSlices decoded
reindexSliceStatusSliceId s0 `shouldBe` Just 0
reindexSliceStatusTotal s0 `shouldBe` Just 150
reindexSliceStatusThrottled s0 `shouldBe` Just "0s"
reindexSliceStatusThrottledUntil s0 `shouldBe` Just "0s"
reindexSliceStatusCancelled s0 `shouldBe` Nothing
reindexSliceStatusSliceId s1 `shouldBe` Just 1
reindexSliceStatusCancelled s1 `shouldBe` Just "user cancelled"
reindexSliceStatusTotal s1 `shouldBe` Nothing
it "round-trips the complete documented response surface" $ do
let Just decoded = decode sampleCompleteResponse :: Maybe ReindexResponse
(decode . encode) decoded `shouldBe` Just decoded
it "decodes a slice carrying a nested retries object" $ do
let bytes =
LBS.pack
( unlines
[ "{\"slice_id\": 0, \"retries\": {\"bulk\": 2, \"search\": 1}}"
]
)
case decode bytes :: Maybe ReindexSliceStatus of
Just s -> do
reindexSliceStatusSliceId s `shouldBe` Just 0
reindexSliceStatusRetries s `shouldSatisfy` isJust
let Just r = reindexSliceStatusRetries s
reindexRetriesBulk r `shouldBe` Just 2
reindexRetriesSearch r `shouldBe` Just 1
Nothing -> fail "expected a decoded ReindexSliceStatus, got Nothing"
it "tolerates a failure entry with no cause" $ do
let bytes = LBS.pack "{\"index\":\"i\",\"id\":\"x\",\"status\":500}"
case decode bytes :: Maybe ReindexFailure of
Just f -> do
reindexFailureIndex f `shouldBe` Just "i"
reindexFailureId f `shouldBe` Just "x"
reindexFailureStatus f `shouldBe` Just 500
reindexFailureCause f `shouldBe` Nothing
Nothing -> fail "expected a decoded ReindexFailure, got Nothing"
describe "ReindexRetries JSON" $ do
it "decodes both fields" $ do
let Just decoded = decode "{\"bulk\": 3, \"search\": 1}" :: Maybe ReindexRetries
reindexRetriesBulk decoded `shouldBe` Just 3
reindexRetriesSearch decoded `shouldBe` Just 1
it "decodes an empty object as all Nothing" $
case decode "{}" :: Maybe ReindexRetries of
Just decoded -> do
reindexRetriesBulk decoded `shouldBe` Nothing
reindexRetriesSearch decoded `shouldBe` Nothing
Nothing -> fail "expected a decoded ReindexRetries, got Nothing"
it "round-trips via encode . decode" $ do
let original = ReindexRetries {reindexRetriesBulk = Just 5, reindexRetriesSearch = Just 2}
(decode . encode) original `shouldBe` Just original
it "never emits keys for Nothing fields" $ do
let encoded = encode (ReindexRetries {reindexRetriesBulk = Nothing, reindexRetriesSearch = Nothing})
case decode encoded :: Maybe ReindexRetries of
Just decoded -> do
reindexRetriesBulk decoded `shouldBe` Nothing
reindexRetriesSearch decoded `shouldBe` Nothing
Nothing -> fail "expected a decoded ReindexRetries, got Nothing"
describe "ReindexFailureCause JSON" $ do
it "decodes type and reason alone" $
case decode "{\"type\":\"mapper_parsing_exception\",\"reason\":\"oops\"}" :: Maybe ReindexFailureCause of
Just c -> do
reindexFailureCauseType c `shouldBe` Just "mapper_parsing_exception"
reindexFailureCauseReason c `shouldBe` Just "oops"
reindexFailureCauseCausedBy c `shouldBe` Nothing
Nothing -> fail "expected a decoded ReindexFailureCause, got Nothing"
it "decodes a recursive caused_by chain" $ do
let bytes =
LBS.pack
( unlines
[ "{",
" \"type\": \"outer\",",
" \"caused_by\": {",
" \"type\": \"middle\",",
" \"caused_by\": {\"type\": \"inner\"}",
" }",
"}"
]
)
case decode bytes :: Maybe ReindexFailureCause of
Just c -> do
reindexFailureCauseType c `shouldBe` Just "outer"
let Just mid = reindexFailureCauseCausedBy c
reindexFailureCauseType mid `shouldBe` Just "middle"
let Just inner = reindexFailureCauseCausedBy mid
reindexFailureCauseType inner `shouldBe` Just "inner"
reindexFailureCauseCausedBy inner `shouldBe` Nothing
Nothing -> fail "expected a decoded ReindexFailureCause, got Nothing"
it "decodes root_cause and suppressed as lists of causes" $ do
let bytes =
LBS.pack
( unlines
[ "{",
" \"type\": \"top\",",
" \"root_cause\": [{\"type\":\"r1\"}],",
" \"suppressed\": [{\"type\":\"s1\"},{\"type\":\"s2\"}]",
"}"
]
)
case decode bytes :: Maybe ReindexFailureCause of
Just c -> do
let Just rc = reindexFailureCauseRootCause c
length rc `shouldBe` 1
let Just sup = reindexFailureCauseSuppressed c
length sup `shouldBe` 2
Nothing -> fail "expected a decoded ReindexFailureCause, got Nothing"
it "round-trips via encode . decode" $ do
let original =
ReindexFailureCause
{ reindexFailureCauseType = Just "t",
reindexFailureCauseReason = Just "r",
reindexFailureCauseStackTrace = Nothing,
reindexFailureCauseCausedBy =
Just
ReindexFailureCause
{ reindexFailureCauseType = Just "inner",
reindexFailureCauseReason = Nothing,
reindexFailureCauseStackTrace = Nothing,
reindexFailureCauseCausedBy = Nothing,
reindexFailureCauseRootCause = Nothing,
reindexFailureCauseSuppressed = Nothing
},
reindexFailureCauseRootCause = Nothing,
reindexFailureCauseSuppressed = Nothing
}
(decode . encode) original `shouldBe` Just original
describe "ReindexFailure JSON" $
it "round-trips via encode . decode" $ do
let original =
ReindexFailure
{ reindexFailureIndex = Just "i",
reindexFailureId = Just "x",
reindexFailureStatus = Just 500,
reindexFailureCause =
Just
ReindexFailureCause
{ reindexFailureCauseType = Just "t",
reindexFailureCauseReason = Just "r",
reindexFailureCauseStackTrace = Nothing,
reindexFailureCauseCausedBy = Nothing,
reindexFailureCauseRootCause = Nothing,
reindexFailureCauseSuppressed = Nothing
}
}
(decode . encode) original `shouldBe` Just original
describe "ReindexSliceStatus JSON" $
it "round-trips via encode . decode" $ do
let original =
ReindexSliceStatus
{ reindexSliceStatusSliceId = Just 0,
reindexSliceStatusBatches = Just 1,
reindexSliceStatusCreated = Just 100,
reindexSliceStatusDeleted = Just 0,
reindexSliceStatusNoops = Just 0,
reindexSliceStatusRequestsPerSecond = Nothing,
reindexSliceStatusRetries =
Just
ReindexRetries
{ reindexRetriesBulk = Just 0,
reindexRetriesSearch = Just 0
},
reindexSliceStatusThrottled = Just "0s",
reindexSliceStatusThrottledMillis = Just 0,
reindexSliceStatusThrottledUntil = Just "0s",
reindexSliceStatusThrottledUntilMillis = Just 0,
reindexSliceStatusTotal = Just 100,
reindexSliceStatusUpdated = Just 0,
reindexSliceStatusVersionConflicts = Just 0,
reindexSliceStatusCancelled = Nothing
}
(decode . encode) original `shouldBe` Just original
describe "malformed inputs" $ do
it "rejects an array" $ do
let decoded = decode "[]" :: Maybe ReindexResponse
decoded `shouldBe` Nothing
it "rejects a bare string" $ do
let decoded = decode "\"not-an-object\"" :: Maybe ReindexResponse
decoded `shouldBe` Nothing
it "rejects null" $ do
let decoded = decode "null" :: Maybe ReindexResponse
decoded `shouldBe` Nothing
it "rejects an array for retries" $ do
let decoded = decode "{\"retries\": []}" :: Maybe ReindexResponse
decoded `shouldBe` Nothing
it "decodes an empty failures list as Just []" $
case decode "{\"failures\": []}" :: Maybe ReindexResponse of
Just r -> reindexResponseFailures r `shouldBe` Just []
Nothing -> fail "expected Just ReindexResponse with empty failures list"
it "rejects an object for slices" $ do
let decoded = decode "{\"slices\": {}}" :: Maybe ReindexResponse
decoded `shouldBe` Nothing
-- Property-based round-trips driven by the Arbitrary instances in
-- TestsUtils.Generators. Confirms ToJSON and FromJSON are exact
-- inverses on randomly generated values, not just hand-rolled samples.
describe "QuickCheck round-trips" $ do
it "ReindexRetries: decode . encode == id" $
property $
\r -> (decode . encode) (r :: ReindexRetries) == Just r
it "ReindexResponse: decode . encode == id" $
property $
\r -> (decode . encode) (r :: ReindexResponse) == Just r
it "ReindexFailureCause: decode . encode == id" $
property $
\r -> (decode . encode) (r :: ReindexFailureCause) == Just r
it "ReindexFailure: decode . encode == id" $
property $
\r -> (decode . encode) (r :: ReindexFailure) == Just r
it "ReindexSliceStatus: decode . encode == id" $
property $
\r -> (decode . encode) (r :: ReindexSliceStatus) == Just r