bloodhound-1.0.0.0: tests/Test/AlertingSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Test.AlertingSpec (spec) where
import Data.Aeson.KeyMap qualified as KM
import Data.ByteString.Lazy.Char8 qualified as LBS
import Data.List (isInfixOf, sortOn)
import Data.Maybe (fromJust, isJust)
import Database.Bloodhound.OpenSearch1.Client qualified as OS1.Client
import Database.Bloodhound.OpenSearch1.Requests qualified as OS1Requests
import Database.Bloodhound.OpenSearch1.Types qualified as OS1A
import Database.Bloodhound.OpenSearch2.Client qualified as OS2.Client
import Database.Bloodhound.OpenSearch2.Requests qualified as OS2Requests
import Database.Bloodhound.OpenSearch2.Types qualified as OS2A
import Database.Bloodhound.OpenSearch3.Client qualified as OS3.Client
import Database.Bloodhound.OpenSearch3.Requests qualified as OS3Requests
import Database.Bloodhound.OpenSearch3.Types qualified as OS3A
import Numeric.Natural (Natural)
import TestsUtils.Common
import TestsUtils.Import
import Prelude
-- | Pure endpoint-shape and decoder tests for the Alerting plugin monitor
-- CRUD endpoints (@POST\/GET\/PUT\/DELETE /_plugins/_alerting/monitors[\/{id}]@)
-- across OpenSearch 1, 2, and 3. Mirrors 'Test.KnnWarmupSpec'\/'Test.IsmSpec'
-- for the shape tests and 'Test.MLModelSpec' for the decoder tests.
--
-- The monitor 'Monitor' is modelled pragmatically: the typed shell (name,
-- enabled, schedule, triggers, actions) plus opaque aeson 'Value's for the
-- per-kind payloads. These tests pin (a) the endpoint shape per backend,
-- (b) OS1\/OS2\/OS3 cross-backend parity, (c) decoder round-trips for the
-- documented wire fixtures, and (d) the wrapper-key discrimination on
-- 'Trigger' (query-level bare vs. bucket-level wrapped).
--
-- The live round-trip is @pendingWith@'d: the CI docker-compose runs
-- Elasticsearch clusters only, so the Alerting plugin (OS-specific) is not
-- available in CI. Re-enable once a live OS cluster is wired in; see the
-- precedent in 'Test.KnnWarmupSpec' \/ 'Test.IsmSpec' live blocks.
spec :: Spec
spec = describe "Alerting monitor CRUD API" $ do
-- =========================================================== --
-- Endpoint shape: OpenSearch 3 (detailed) --
-- =========================================================== --
describe "endpoint shape (OpenSearch 3)" $ do
it "createMonitor POSTs to /_plugins/_alerting/monitors with the encoded body" $ do
let req = OS3Requests.createMonitor os3SampleMonitor
bhRequestMethod req `shouldBe` "POST"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "monitors"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
-- Body is present and decodes back to the same monitor on the typed fields.
let body = fromJust (bhRequestBody req)
let decoded = (decode body :: Maybe OS3A.Monitor)
decoded `shouldSatisfy` \m -> isJust m && OS3A.monitorName (fromJust m) == "test-monitor"
it "getMonitor GETs /_plugins/_alerting/monitors/{id} with no body" $ do
let req = OS3Requests.getMonitor (OS3A.MonitorId "my-monitor-id")
bhRequestMethod req `shouldBe` "GET"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "monitors", "my-monitor-id"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
bhRequestBody req `shouldBe` Nothing
it "getMonitor keeps the id as a single opaque path segment" $ do
let req = OS3Requests.getMonitor (OS3A.MonitorId "my-monitor_42")
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "monitors", "my-monitor_42"]
it "getMonitor uses a distinct id in the path for a different MonitorId" $ do
let req = OS3Requests.getMonitor (OS3A.MonitorId "other-id")
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "monitors", "other-id"]
it "updateMonitor PUTs /_plugins/_alerting/monitors/{id} with the encoded body" $ do
let req = OS3Requests.updateMonitor (OS3A.MonitorId "my-monitor-id") os3SampleMonitor
bhRequestMethod req `shouldBe` "PUT"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "monitors", "my-monitor-id"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
bhRequestBody req `shouldSatisfy` isJust
it "deleteMonitor DELETEs /_plugins/_alerting/monitors/{id} with no body" $ do
let req = OS3Requests.deleteMonitor (OS3A.MonitorId "my-monitor-id")
bhRequestMethod req `shouldBe` "DELETE"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "monitors", "my-monitor-id"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
bhRequestBody req `shouldBe` Nothing
-- =========================================================== --
-- Endpoint shape: OpenSearch 1 and 2 (path/method parity) --
-- =========================================================== --
describe "endpoint shape (OpenSearch 1)" $ do
it "getMonitor GETs /_plugins/_alerting/monitors/{id}" $ do
let req = OS1Requests.getMonitor (OS1A.MonitorId "os1-id")
bhRequestMethod req `shouldBe` "GET"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "monitors", "os1-id"]
bhRequestBody req `shouldBe` Nothing
it "deleteMonitor DELETEs /_plugins/_alerting/monitors/{id}" $ do
let req = OS1Requests.deleteMonitor (OS1A.MonitorId "os1-id")
bhRequestMethod req `shouldBe` "DELETE"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "monitors", "os1-id"]
it "createMonitor POSTs to /_plugins/_alerting/monitors" $ do
let req = OS1Requests.createMonitor os1SampleMonitor
bhRequestMethod req `shouldBe` "POST"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "monitors"]
it "updateMonitor PUTs /_plugins/_alerting/monitors/{id}" $ do
let req = OS1Requests.updateMonitor (OS1A.MonitorId "os1-id") os1SampleMonitor
bhRequestMethod req `shouldBe` "PUT"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "monitors", "os1-id"]
describe "endpoint shape (OpenSearch 2)" $ do
it "getMonitor GETs /_plugins/_alerting/monitors/{id}" $ do
let req = OS2Requests.getMonitor (OS2A.MonitorId "os2-id")
bhRequestMethod req `shouldBe` "GET"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "monitors", "os2-id"]
bhRequestBody req `shouldBe` Nothing
it "deleteMonitor DELETEs /_plugins/_alerting/monitors/{id}" $ do
let req = OS2Requests.deleteMonitor (OS2A.MonitorId "os2-id")
bhRequestMethod req `shouldBe` "DELETE"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "monitors", "os2-id"]
it "createMonitor POSTs to /_plugins/_alerting/monitors" $ do
let req = OS2Requests.createMonitor os2SampleMonitor
bhRequestMethod req `shouldBe` "POST"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "monitors"]
it "updateMonitor PUTs /_plugins/_alerting/monitors/{id}" $ do
let req = OS2Requests.updateMonitor (OS2A.MonitorId "os2-id") os2SampleMonitor
bhRequestMethod req `shouldBe` "PUT"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "monitors", "os2-id"]
-- =========================================================== --
-- Cross-backend parity (OS1, OS2, OS3 emit identical requests) --
-- =========================================================== --
describe "cross-backend parity" $ do
let mid1 = OS1A.MonitorId "shared-id"
mid2 = OS2A.MonitorId "shared-id"
mid3 = OS3A.MonitorId "shared-id"
it "createMonitor: OS1, OS2, OS3 produce the same path and method" $ do
getRawEndpoint (bhRequestEndpoint (OS1Requests.createMonitor os1SampleMonitor))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS2Requests.createMonitor os2SampleMonitor))
getRawEndpoint (bhRequestEndpoint (OS2Requests.createMonitor os2SampleMonitor))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS3Requests.createMonitor os3SampleMonitor))
bhRequestMethod (OS1Requests.createMonitor os1SampleMonitor)
`shouldBe` bhRequestMethod (OS3Requests.createMonitor os3SampleMonitor)
it "createMonitor: OS1, OS2, OS3 attach identical body bytes" $ do
-- The three Monitors are structurally identical, so their encoded
-- bodies must match byte-for-byte across the three backends.
let b1 = bhRequestBody (OS1Requests.createMonitor os1SampleMonitor)
b2 = bhRequestBody (OS2Requests.createMonitor os2SampleMonitor)
b3 = bhRequestBody (OS3Requests.createMonitor os3SampleMonitor)
b1 `shouldBe` b2
b2 `shouldBe` b3
it "getMonitor: OS1, OS2, OS3 produce the same path and method" $ do
getRawEndpoint (bhRequestEndpoint (OS1Requests.getMonitor mid1))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS2Requests.getMonitor mid2))
getRawEndpoint (bhRequestEndpoint (OS2Requests.getMonitor mid2))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS3Requests.getMonitor mid3))
bhRequestMethod (OS1Requests.getMonitor mid1)
`shouldBe` bhRequestMethod (OS3Requests.getMonitor mid3)
it "deleteMonitor: OS1, OS2, OS3 produce the same path and method" $ do
getRawEndpoint (bhRequestEndpoint (OS1Requests.deleteMonitor mid1))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS2Requests.deleteMonitor mid2))
getRawEndpoint (bhRequestEndpoint (OS2Requests.deleteMonitor mid2))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS3Requests.deleteMonitor mid3))
bhRequestMethod (OS1Requests.deleteMonitor mid1)
`shouldBe` bhRequestMethod (OS3Requests.deleteMonitor mid3)
-- =========================================================== --
-- Decoder tests (OS3 types — identical instances across OS1-3) --
-- =========================================================== --
describe "MonitorResponse decoding" $ do
let fixture =
LBS.pack
"{\"_id\":\"vd5k2GsBlQ5JUWWFxhsP\",\"_version\":1,\"_seq_no\":0,\"_primary_term\":1,\"monitor\":{\"type\":\"monitor\",\"schema_version\":1,\"name\":\"test-monitor\",\"monitor_type\":\"query_level_monitor\",\"enabled\":true,\"enabled_time\":1551466220455,\"schedule\":{\"period\":{\"interval\":1,\"unit\":\"MINUTES\"}},\"inputs\":[],\"triggers\":[],\"last_update_time\":1551466639295}}"
it "decodes the documented create-response fixture" $ do
let Just decoded = decode fixture :: Maybe OS3A.MonitorResponse
OS3A.monitorResponseId decoded `shouldBe` "vd5k2GsBlQ5JUWWFxhsP"
OS3A.monitorResponseVersion decoded `shouldBe` 1
OS3A.monitorResponseSeqNo decoded `shouldBe` 0
OS3A.monitorResponsePrimaryTerm decoded `shouldBe` 1
let mon = OS3A.monitorResponseMonitor decoded
OS3A.monitorName mon `shouldBe` "test-monitor"
OS3A.monitorEnabled mon `shouldBe` Just True
OS3A.monitorEnabledTime mon `shouldBe` Just 1551466220455
OS3A.monitorMonitorType mon `shouldBe` Just OS3A.MonitorTypeQueryLevel
OS3A.monitorType mon `shouldBe` Just OS3A.AlertTypeMonitor
OS3A.monitorSchemaVersion mon `shouldBe` Just 1
it "monitor round-trips through encode/decode on the typed fields" $ do
let encoded = encode os3SampleMonitor
Just reDecoded = decode encoded :: Maybe OS3A.Monitor
OS3A.monitorName reDecoded `shouldBe` OS3A.monitorName os3SampleMonitor
OS3A.monitorSchedule reDecoded `shouldBe` OS3A.monitorSchedule os3SampleMonitor
OS3A.monitorMonitorType reDecoded `shouldBe` OS3A.monitorMonitorType os3SampleMonitor
describe "DeleteMonitorResponse decoding" $ do
let fixture =
LBS.pack
"{\"_index\":\".opensearch-scheduled-jobs\",\"_id\":\"vd5k2GsBlQ5JUWWFxhsP\",\"_version\":2,\"result\":\"deleted\",\"forced_refresh\":false,\"_shards\":{\"total\":2,\"successful\":1,\"skipped\":0,\"failed\":0},\"_seq_no\":1,\"_primary_term\":1}"
it "decodes the full bare delete-document fixture (OS 1.3.x shape)" $ do
let Just decoded = decode fixture :: Maybe OS3A.DeleteMonitorResponse
OS3A.deleteMonitorResponseIndex decoded `shouldBe` Just ".opensearch-scheduled-jobs"
OS3A.deleteMonitorResponseId decoded `shouldBe` "vd5k2GsBlQ5JUWWFxhsP"
OS3A.deleteMonitorResponseVersion decoded `shouldBe` 2
OS3A.deleteMonitorResponseResult decoded `shouldBe` Just "deleted"
OS3A.deleteMonitorResponseForcedRefresh decoded `shouldBe` Just False
let Just sh = OS3A.deleteMonitorResponseShards decoded
OS3A.alertingShardsTotal sh `shouldBe` 2
OS3A.alertingShardsSuccessful sh `shouldBe` 1
OS3A.alertingShardsFailed sh `shouldBe` 0
OS3A.deleteMonitorResponseSeqNo decoded `shouldBe` Just 1
OS3A.deleteMonitorResponsePrimaryTerm decoded `shouldBe` Just 1
it "decodes the minimal {_id,_version} fixture (OS 2.x/3.x shape, live-verified bead bloodhound-z5j)" $ do
let minimal = LBS.pack "{\"_id\":\"abc123\",\"_version\":2}"
Just decoded = decode minimal :: Maybe OS3A.DeleteMonitorResponse
OS3A.deleteMonitorResponseId decoded `shouldBe` "abc123"
OS3A.deleteMonitorResponseVersion decoded `shouldBe` 2
OS3A.deleteMonitorResponseIndex decoded `shouldBe` Nothing
OS3A.deleteMonitorResponseResult decoded `shouldBe` Nothing
OS3A.deleteMonitorResponseForcedRefresh decoded `shouldBe` Nothing
OS3A.deleteMonitorResponseShards decoded `shouldBe` Nothing
OS3A.deleteMonitorResponseSeqNo decoded `shouldBe` Nothing
OS3A.deleteMonitorResponsePrimaryTerm decoded `shouldBe` Nothing
it "rejects a payload missing _id (the sole universally-required field)" $ do
let decoded = decode "{\"_version\":2}" :: Maybe OS3A.DeleteMonitorResponse
decoded `shouldBe` Nothing
describe "Schedule decoding" $ do
it "decodes a period schedule" $ do
let Just s = decode "{\"period\":{\"interval\":5,\"unit\":\"HOURS\"}}" :: Maybe OS3A.Schedule
Just (OS3A.PeriodSchedule p) = Just s
OS3A.schedulePeriodInterval p `shouldBe` 5
OS3A.schedulePeriodUnit p `shouldBe` "HOURS"
it "decodes a cron schedule" $ do
let Just s = decode "{\"cron\":{\"expression\":\"10 12 1 * *\",\"timezone\":\"America/Los_Angeles\"}}" :: Maybe OS3A.Schedule
Just (OS3A.CronSchedule c) = Just s
OS3A.scheduleCronExpression c `shouldBe` "10 12 1 * *"
OS3A.scheduleCronTimezone c `shouldBe` Just "America/Los_Angeles"
it "falls through to OtherSchedule for an unknown schedule kind" $ do
let Just s = decode "{\"custom\":{\"foo\":1}}" :: Maybe OS3A.Schedule
Just (OS3A.OtherSchedule _) = Just s
s `shouldBe` OS3A.OtherSchedule (object ["custom" .= object ["foo" .= (1 :: Int)]])
describe "MonitorType discriminator" $ do
it "decodes each documented monitor_type value" $ do
"query_level_monitor" `decodesMonitorTypeTo` OS3A.MonitorTypeQueryLevel
"bucket_level_monitor" `decodesMonitorTypeTo` OS3A.MonitorTypeBucketLevel
"doc_level_monitor" `decodesMonitorTypeTo` OS3A.MonitorTypeDocLevel
"cluster_metrics_monitor" `decodesMonitorTypeTo` OS3A.MonitorTypeClusterMetrics
"remote_monitor" `decodesMonitorTypeTo` OS3A.MonitorTypeRemote
it "round-trips through monitorTypeText" $
OS3A.monitorTypeText OS3A.MonitorTypeQueryLevel `shouldBe` "query_level_monitor"
it "falls through to MonitorTypeOther for an unknown value" $
"future_monitor_kind" `decodesMonitorTypeTo` OS3A.MonitorTypeOther "future_monitor_kind"
describe "Trigger wrapper-key discrimination" $ do
it "decodes a bare query-level trigger (fields directly on the element)" $ do
let fixture =
LBS.pack
"{\"name\":\"t\",\"severity\":\"1\",\"condition\":{\"script\":{\"source\":\"ctx.results[0] > 0\",\"lang\":\"painless\"}},\"actions\":[]}"
let Just t = decode fixture :: Maybe OS3A.Trigger
OS3A.triggerName t `shouldBe` "t"
OS3A.triggerSeverity t `shouldBe` "1"
OS3A.triggerActions t `shouldBe` []
it "decodes a wrapped bucket-level trigger (fields under bucket_level_trigger)" $ do
let fixture =
LBS.pack
"{\"bucket_level_trigger\":{\"name\":\"bt\",\"severity\":\"2\",\"condition\":{\"buckets_path\":{\"_count\":\"_count\"},\"parent_bucket_path\":\"agg\"},\"actions\":[]}}"
let Just t = decode fixture :: Maybe OS3A.Trigger
OS3A.triggerName t `shouldBe` "bt"
OS3A.triggerSeverity t `shouldBe` "2"
it "decodes a wrapped document-level trigger (fields under document_level_trigger)" $ do
let fixture =
LBS.pack
"{\"document_level_trigger\":{\"name\":\"dt\",\"severity\":\"3\",\"condition\":{\"script\":{\"source\":\"true\",\"lang\":\"painless\"}},\"actions\":[]}}"
let Just t = decode fixture :: Maybe OS3A.Trigger
OS3A.triggerName t `shouldBe` "dt"
OS3A.triggerSeverity t `shouldBe` "3"
it "round-trips a wrapped bucket-level trigger losslessly" $ do
let fixture =
LBS.pack
"{\"bucket_level_trigger\":{\"name\":\"bt\",\"severity\":\"2\",\"condition\":{\"buckets_path\":{}},\"actions\":[]}}"
let Just t = decode fixture :: Maybe OS3A.Trigger
-- The wrapper key must survive the encode so the discrimination round-trips.
encode t `shouldSatisfy` \bs -> "bucket_level_trigger" `isInfixOf` LBS.unpack bs
it "severity is decoded as a string (not a number) per the wire" $ do
let Just t = decode "{\"name\":\"x\",\"severity\":\"1\",\"condition\":{},\"actions\":[]}" :: Maybe OS3A.Trigger
OS3A.triggerSeverity t `shouldBe` "1"
describe "Action decoding" $ do
it "decodes a full action fixture" $ do
let fixture =
LBS.pack
"{\"name\":\"test-action\",\"destination_id\":\"ld7912sBlQ5JUWWFThoW\",\"message_template\":{\"source\":\"Body.\",\"lang\":\"mustache\"},\"subject_template\":{\"source\":\"Subject\",\"lang\":\"mustache\"},\"throttle_enabled\":true,\"throttle\":{\"value\":27,\"unit\":\"MINUTES\"}}"
let Just a = decode fixture :: Maybe OS3A.Action
OS3A.actionName a `shouldBe` "test-action"
OS3A.actionDestinationId a `shouldBe` "ld7912sBlQ5JUWWFThoW"
OS3A.actionThrottleEnabled a `shouldBe` Just True
let Just th = OS3A.actionThrottle a
OS3A.throttleValue th `shouldBe` 27
OS3A.throttleUnit th `shouldBe` "MINUTES"
-- =========================================================== --
-- Destinations: GET /_plugins/_alerting/destinations --
-- Endpoint shape, decoders, type round-trip, query params. --
-- =========================================================== --
describe "getDestinations endpoint shape (OpenSearch 3)" $ do
it "getDestinations GETs /_plugins/_alerting/destinations with no body" $ do
let req = OS3Requests.getDestinations
bhRequestMethod req `shouldBe` "GET"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "destinations"]
bhRequestBody req `shouldBe` Nothing
it "getDestinations with default options emits no query params" $ do
let req = OS3Requests.getDestinationsWith OS3A.defaultDestinationListOptions
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "getDestinationsWith forwards options as camelCase query params" $ do
let opts =
OS3A.defaultDestinationListOptions
{ OS3A.destinationListOptionsSize = Just 5,
OS3A.destinationListOptionsStartIndex = Just 10,
OS3A.destinationListOptionsSortString = Just "destination.name.keyword",
OS3A.destinationListOptionsSortOrder = Just OS3A.DestinationSortOrderDesc,
OS3A.destinationListOptionsMissing = Just "_last",
OS3A.destinationListOptionsSearchString = Just "alert",
OS3A.destinationListOptionsDestinationType = Just OS3A.DestinationTypeSlack
}
let req = OS3Requests.getDestinationsWith opts
-- Order is not significant; sort to compare deterministically.
sortOn fst (getRawEndpointQueries (bhRequestEndpoint req))
`shouldBe` sortOn
fst
[ ("destinationType", Just "slack"),
("missing", Just "_last"),
("searchString", Just "alert"),
("size", Just "5"),
("sortString", Just "destination.name.keyword"),
("sortOrder", Just "desc"),
("start_index", Just "10")
]
it "getDestinationsWith omits fields set to Nothing (not rendered with empty values)" $ do
let opts =
OS3A.defaultDestinationListOptions
{ OS3A.destinationListOptionsSize = Just 5
}
let req = OS3Requests.getDestinationsWith opts
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` [("size", Just "5")]
describe "getDestinations endpoint shape (OpenSearch 1 and 2 parity)" $ do
it "OS1 getDestinations GETs /_plugins/_alerting/destinations" $ do
let req = OS1Requests.getDestinations
bhRequestMethod req `shouldBe` "GET"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "destinations"]
bhRequestBody req `shouldBe` Nothing
it "OS2 getDestinations GETs /_plugins/_alerting/destinations" $ do
let req = OS2Requests.getDestinations
bhRequestMethod req `shouldBe` "GET"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "destinations"]
bhRequestBody req `shouldBe` Nothing
it "OS1, OS2, OS3 produce the same path and method" $ do
getRawEndpoint (bhRequestEndpoint OS1Requests.getDestinations)
`shouldBe` getRawEndpoint (bhRequestEndpoint OS2Requests.getDestinations)
getRawEndpoint (bhRequestEndpoint OS2Requests.getDestinations)
`shouldBe` getRawEndpoint (bhRequestEndpoint OS3Requests.getDestinations)
bhRequestMethod OS1Requests.getDestinations
`shouldBe` bhRequestMethod OS3Requests.getDestinations
describe "DestinationType discriminator" $ do
it "round-trips each documented value through destinationTypeText" $ do
OS3A.destinationTypeText OS3A.DestinationTypeSlack `shouldBe` "slack"
OS3A.destinationTypeText OS3A.DestinationTypeChime `shouldBe` "chime"
OS3A.destinationTypeText OS3A.DestinationTypeCustomWebhook `shouldBe` "custom_webhook"
OS3A.destinationTypeText OS3A.DestinationTypeEmail `shouldBe` "email"
OS3A.destinationTypeText OS3A.DestinationTypeTestAction `shouldBe` "test_action"
it "decodes each documented wire value" $ do
"slack" `decodesDestinationTypeTo` OS3A.DestinationTypeSlack
"chime" `decodesDestinationTypeTo` OS3A.DestinationTypeChime
"custom_webhook" `decodesDestinationTypeTo` OS3A.DestinationTypeCustomWebhook
"email" `decodesDestinationTypeTo` OS3A.DestinationTypeEmail
"test_action" `decodesDestinationTypeTo` OS3A.DestinationTypeTestAction
it "falls through to DestinationTypeOther for an unknown value" $
"future_dest_kind" `decodesDestinationTypeTo` OS3A.DestinationTypeOther "future_dest_kind"
describe "Destination decoding" $ do
it "decodes the documented Slack fixture" $ do
let fixture =
LBS.pack
"{\"id\":\"1a2a3a4a5a6a7a\",\"type\":\"slack\",\"name\":\"sample-destination\",\"schema_version\":3,\"seq_no\":0,\"primary_term\":6,\"last_update_time\":1603943261722,\"slack\":{\"url\":\"https://example.com\"}}"
let Just d = decode fixture :: Maybe OS3A.Destination
OS3A.destinationId d `shouldBe` "1a2a3a4a5a6a7a"
OS3A.destinationType d `shouldBe` OS3A.DestinationTypeSlack
OS3A.destinationName d `shouldBe` "sample-destination"
OS3A.destinationSchemaVersion d `shouldBe` 3
OS3A.destinationSeqNo d `shouldBe` 0
OS3A.destinationPrimaryTerm d `shouldBe` 6
OS3A.destinationLastUpdateTime d `shouldBe` Just 1603943261722
OS3A.destinationBody d `shouldBe` object ["url" .= ("https://example.com" :: Text)]
it "decodes a Chime destination" $ do
let fixture =
LBS.pack
"{\"id\":\"ch1\",\"type\":\"chime\",\"name\":\"chime-dest\",\"schema_version\":3,\"seq_no\":1,\"primary_term\":1,\"chime\":{\"url\":\"https://hooks.chime.aws/incomingwebhooks/x\"}}"
let Just d = decode fixture :: Maybe OS3A.Destination
OS3A.destinationType d `shouldBe` OS3A.DestinationTypeChime
OS3A.destinationBody d `shouldBe` object ["url" .= ("https://hooks.chime.aws/incomingwebhooks/x" :: Text)]
it "decodes a Custom Webhook destination (full sub-object)" $ do
let fixture =
LBS.pack
"{\"id\":\"cw1\",\"type\":\"custom_webhook\",\"name\":\"cw-dest\",\"schema_version\":3,\"seq_no\":0,\"primary_term\":1,\"custom_webhook\":{\"url\":\"https://example.com/hook\",\"scheme\":\"HTTPS\",\"host\":\"example.com\",\"port\":443,\"path\":\"hook\",\"method\":\"POST\",\"query_params\":{\"token\":\"abc\"},\"header_params\":{\"Content-Type\":\"application/json\"}}}"
let Just d = decode fixture :: Maybe OS3A.Destination
OS3A.destinationType d `shouldBe` OS3A.DestinationTypeCustomWebhook
OS3A.destinationBody d
`shouldSatisfy` \body -> "\"url\"" `isInfixOf` LBS.unpack (encode body)
it "decodes an Email destination" $ do
let fixture =
LBS.pack
"{\"id\":\"em1\",\"type\":\"email\",\"name\":\"email-dest\",\"schema_version\":3,\"seq_no\":0,\"primary_term\":1,\"email\":{\"email_account_id\":\"YjY7mXMBx015759_IcfW\",\"recipients\":[{\"type\":\"email\",\"email\":\"example@email.com\"}]}}"
let Just d = decode fixture :: Maybe OS3A.Destination
OS3A.destinationType d `shouldBe` OS3A.DestinationTypeEmail
OS3A.destinationBody d
`shouldSatisfy` \body -> "\"email_account_id\"" `isInfixOf` LBS.unpack (encode body)
it "decodes a test_action destination (no sub-object, body=Null)" $ do
let fixture =
LBS.pack
"{\"id\":\"ta1\",\"type\":\"test_action\",\"name\":\"ta-dest\",\"schema_version\":3,\"seq_no\":0,\"primary_term\":1}"
let Just d = decode fixture :: Maybe OS3A.Destination
OS3A.destinationType d `shouldBe` OS3A.DestinationTypeTestAction
OS3A.destinationBody d `shouldBe` Null
it "decodes the documented fixture with a user sub-object" $ do
let fixture =
LBS.pack
"{\"id\":\"1a2a3a4a5a6a7a\",\"type\":\"slack\",\"name\":\"sample-destination\",\"user\":{\"name\":\"psantos\",\"backend_roles\":[\"human-resources\"],\"roles\":[\"alerting_full_access\"]},\"schema_version\":3,\"seq_no\":0,\"primary_term\":6,\"last_update_time\":1603943261722,\"slack\":{\"url\":\"https://example.com\"}}"
let Just d = decode fixture :: Maybe OS3A.Destination
OS3A.destinationUser d
`shouldSatisfy` \case
Just _ -> True
_ -> False
it "tolerates missing schema_version, seq_no, primary_term (default 0)" $ do
let fixture =
LBS.pack
"{\"id\":\"x\",\"type\":\"slack\",\"name\":\"x\",\"slack\":{\"url\":\"https://example.com\"}}"
let Just d = decode fixture :: Maybe OS3A.Destination
OS3A.destinationSchemaVersion d `shouldBe` 0
OS3A.destinationSeqNo d `shouldBe` 0
OS3A.destinationPrimaryTerm d `shouldBe` 0
it "round-trips through encode/decode on the typed fields" $ do
let encoded = encode os3SampleSlackDestination
Just reDecoded = decode encoded :: Maybe OS3A.Destination
OS3A.destinationId reDecoded `shouldBe` OS3A.destinationId os3SampleSlackDestination
OS3A.destinationType reDecoded `shouldBe` OS3A.destinationType os3SampleSlackDestination
OS3A.destinationName reDecoded `shouldBe` OS3A.destinationName os3SampleSlackDestination
OS3A.destinationBody reDecoded `shouldBe` OS3A.destinationBody os3SampleSlackDestination
it "re-encodes the per-type sub-object under its discriminator key" $ do
let encoded = encode os3SampleSlackDestination
-- The slack sub-object key must survive the round-trip.
encoded `shouldSatisfy` \bs -> "\"slack\"" `isInfixOf` LBS.unpack bs
describe "GetDestinationsResponse envelope decoding" $ do
it "decodes the documented envelope" $ do
let fixture =
LBS.pack
"{\"totalDestinations\":1,\"destinations\":[{\"id\":\"1a2a3a4a5a6a7a\",\"type\":\"slack\",\"name\":\"sample-destination\",\"schema_version\":3,\"seq_no\":0,\"primary_term\":6,\"last_update_time\":1603943261722,\"slack\":{\"url\":\"https://example.com\"}}]}"
let Just resp = decode fixture :: Maybe OS3A.GetDestinationsResponse
OS3A.getDestinationsResponseTotalDestinations resp `shouldBe` 1
length (OS3A.getDestinationsResponseDestinations resp) `shouldBe` 1
it "decodes an empty destinations list" $ do
let fixture = LBS.pack "{\"totalDestinations\":0,\"destinations\":[]}"
let Just resp = decode fixture :: Maybe OS3A.GetDestinationsResponse
OS3A.getDestinationsResponseTotalDestinations resp `shouldBe` 0
OS3A.getDestinationsResponseDestinations resp `shouldBe` []
it "tolerates a missing totalDestinations field (defaults to 0)" $ do
let fixture = LBS.pack "{\"destinations\":[]}"
let Just resp = decode fixture :: Maybe OS3A.GetDestinationsResponse
OS3A.getDestinationsResponseTotalDestinations resp `shouldBe` 0
describe "destinationListOptionsParams rendering" $ do
it "default options render to an empty parameter list" $
OS3A.destinationListOptionsParams OS3A.defaultDestinationListOptions `shouldBe` []
it "renders all set fields with the documented camelCase wire names" $ do
let opts =
OS3A.defaultDestinationListOptions
{ OS3A.destinationListOptionsSize = Just 50,
OS3A.destinationListOptionsStartIndex = Just 0,
OS3A.destinationListOptionsSortString = Just "destination.type.keyword",
OS3A.destinationListOptionsSortOrder = Just OS3A.DestinationSortOrderAsc,
OS3A.destinationListOptionsMissing = Just "_first",
OS3A.destinationListOptionsSearchString = Just "slack",
OS3A.destinationListOptionsDestinationType = Just OS3A.DestinationTypeChime
}
-- Sorted for determinism; the actual emission order is not significant
-- for the alerting plugin's @RestGetDestinationsAction@.
sortOn fst (OS3A.destinationListOptionsParams opts)
`shouldBe` sortOn
fst
[ ("size", Just "50"),
("start_index", Just "0"),
("sortString", Just "destination.type.keyword"),
("sortOrder", Just "asc"),
("missing", Just "_first"),
("searchString", Just "slack"),
("destinationType", Just "chime")
]
-- =========================================================== --
-- updateMonitorWith: optimistic-concurrency query params. --
-- =========================================================== --
describe "updateMonitorWith endpoint shape (OpenSearch 3)" $ do
it "emits if_seq_no and if_primary_term query params when Just" $ do
let req = OS3Requests.updateMonitorWith (OS3A.MonitorId "mid") os3SampleMonitor (Just (3, 2))
bhRequestMethod req `shouldBe` "PUT"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "monitors", "mid"]
sortOn fst (getRawEndpointQueries (bhRequestEndpoint req))
`shouldBe` [("if_primary_term", Just "2"), ("if_seq_no", Just "3")]
it "omits query params when Nothing" $ do
let req = OS3Requests.updateMonitorWith (OS3A.MonitorId "mid") os3SampleMonitor Nothing
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "updateMonitor is the Nothing special-case (delegation regression)" $ do
let req = OS3Requests.updateMonitor (OS3A.MonitorId "mid") os3SampleMonitor
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
bhRequestMethod req `shouldBe` "PUT"
-- =========================================================== --
-- searchMonitors / executeMonitor endpoint shape. --
-- =========================================================== --
describe "searchMonitors endpoint shape (OpenSearch 3)" $ do
it "GETs /_plugins/_alerting/monitors/_search with {} body when Nothing" $ do
let req = OS3Requests.searchMonitors Nothing
bhRequestMethod req `shouldBe` "GET"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "monitors", "_search"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
let Just body = bhRequestBody req
body `shouldBe` "{}"
it "forwards the encoded MonitorsSearchQuery body when Just" $ do
let q = OS3A.defaultMonitorsSearchQuery {OS3A.monitorsSearchQuerySize = Just 5}
let req = OS3Requests.searchMonitors (Just q)
bhRequestMethod req `shouldBe` "GET"
let Just body = bhRequestBody req
body `shouldBe` encode q
describe "executeMonitor endpoint shape (OpenSearch 3)" $ do
it "POSTs to /_plugins/_alerting/monitors/{id}/_execute with no body and no query when Nothing" $ do
let req = OS3Requests.executeMonitor (OS3A.MonitorId "my-monitor-id") Nothing
bhRequestMethod req `shouldBe` "POST"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "monitors", "my-monitor-id", "_execute"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
bhRequestBody req `shouldBe` Nothing
it "keeps the id as a single opaque path segment" $ do
let req = OS3Requests.executeMonitor (OS3A.MonitorId "my-monitor_42") Nothing
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "monitors", "my-monitor_42", "_execute"]
it "emits the ?dryrun=true query param and no body when dryrun is Just True" $ do
let r = OS3A.defaultExecuteMonitorRequest {OS3A.executeMonitorRequestDryrun = Just True}
let req = OS3Requests.executeMonitor (OS3A.MonitorId "mid") (Just r)
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` [("dryrun", Just "true")]
bhRequestBody req `shouldBe` Nothing
it "omits the dryrun query param when dryrun is Just False" $ do
let r = OS3A.defaultExecuteMonitorRequest {OS3A.executeMonitorRequestDryrun = Just False}
let req = OS3Requests.executeMonitor (OS3A.MonitorId "mid") (Just r)
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
bhRequestBody req `shouldBe` Nothing
-- =========================================================== --
-- Destination lifecycle (create/update/delete) endpoint shape. --
-- =========================================================== --
describe "destination lifecycle endpoint shape (OpenSearch 3)" $ do
it "createDestination POSTs to /_plugins/_alerting/destinations with the encoded body" $ do
let req = OS3Requests.createDestination os3SampleSlackDestination
bhRequestMethod req `shouldBe` "POST"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "destinations"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
bhRequestBody req `shouldSatisfy` isJust
it "createDestination strips the server-assigned keys (id, schema_version, seq_no, primary_term)" $ do
let req = OS3Requests.createDestination os3SampleSlackDestination
Just body = bhRequestBody req
Just (Object o) = decode body :: Maybe Value
KM.lookup "id" o `shouldBe` Nothing
KM.lookup "schema_version" o `shouldBe` Nothing
KM.lookup "seq_no" o `shouldBe` Nothing
KM.lookup "primary_term" o `shouldBe` Nothing
KM.lookup "name" o `shouldNotBe` Nothing
KM.lookup "type" o `shouldNotBe` Nothing
KM.lookup "slack" o `shouldNotBe` Nothing
it "updateDestination PUTs /_plugins/_alerting/destinations/{id} with the encoded body" $ do
let req = OS3Requests.updateDestination (OS3A.DestinationId "dest-1") os3SampleSlackDestination
bhRequestMethod req `shouldBe` "PUT"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "destinations", "dest-1"]
bhRequestBody req `shouldSatisfy` isJust
it "updateDestination keeps the server-assigned keys (only create strips them)" $ do
let req = OS3Requests.updateDestination (OS3A.DestinationId "dest-1") os3SampleSlackDestination
Just body = bhRequestBody req
Just (Object o) = decode body :: Maybe Value
KM.lookup "id" o `shouldNotBe` Nothing
KM.lookup "schema_version" o `shouldNotBe` Nothing
it "deleteDestination DELETEs /_plugins/_alerting/destinations/{id} with no body" $ do
let req = OS3Requests.deleteDestination (OS3A.DestinationId "dest-1")
bhRequestMethod req `shouldBe` "DELETE"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "destinations", "dest-1"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
bhRequestBody req `shouldBe` Nothing
-- =========================================================== --
-- Cross-backend parity for the new endpoints (OS1=OS2=OS3). --
-- =========================================================== --
describe "cross-backend parity (new endpoints)" $ do
let mid1 = OS1A.MonitorId "shared-id"
mid2 = OS2A.MonitorId "shared-id"
mid3 = OS3A.MonitorId "shared-id"
did1 = OS1A.DestinationId "shared-dest"
did2 = OS2A.DestinationId "shared-dest"
did3 = OS3A.DestinationId "shared-dest"
it "searchMonitors: OS1, OS2, OS3 produce the same path and method" $ do
getRawEndpoint (bhRequestEndpoint (OS1Requests.searchMonitors Nothing))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS2Requests.searchMonitors Nothing))
getRawEndpoint (bhRequestEndpoint (OS2Requests.searchMonitors Nothing))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS3Requests.searchMonitors Nothing))
bhRequestMethod (OS1Requests.searchMonitors Nothing)
`shouldBe` bhRequestMethod (OS3Requests.searchMonitors Nothing)
it "executeMonitor: OS1, OS2, OS3 produce the same path and method" $ do
getRawEndpoint (bhRequestEndpoint (OS1Requests.executeMonitor mid1 Nothing))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS2Requests.executeMonitor mid2 Nothing))
getRawEndpoint (bhRequestEndpoint (OS2Requests.executeMonitor mid2 Nothing))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS3Requests.executeMonitor mid3 Nothing))
bhRequestMethod (OS1Requests.executeMonitor mid1 Nothing)
`shouldBe` bhRequestMethod (OS3Requests.executeMonitor mid3 Nothing)
it "executeMonitor: OS1, OS2, OS3 emit the same ?dryrun=true query param" $ do
let d1 = OS1A.defaultExecuteMonitorRequest {OS1A.executeMonitorRequestDryrun = Just True}
d2 = OS2A.defaultExecuteMonitorRequest {OS2A.executeMonitorRequestDryrun = Just True}
d3 = OS3A.defaultExecuteMonitorRequest {OS3A.executeMonitorRequestDryrun = Just True}
q1 = getRawEndpointQueries (bhRequestEndpoint (OS1Requests.executeMonitor mid1 (Just d1)))
q2 = getRawEndpointQueries (bhRequestEndpoint (OS2Requests.executeMonitor mid2 (Just d2)))
q3 = getRawEndpointQueries (bhRequestEndpoint (OS3Requests.executeMonitor mid3 (Just d3)))
q1 `shouldBe` [("dryrun", Just "true")]
q2 `shouldBe` q1
q3 `shouldBe` q1
it "deleteDestination: OS1, OS2, OS3 produce the same path and method" $ do
getRawEndpoint (bhRequestEndpoint (OS1Requests.deleteDestination did1))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS2Requests.deleteDestination did2))
getRawEndpoint (bhRequestEndpoint (OS2Requests.deleteDestination did2))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS3Requests.deleteDestination did3))
bhRequestMethod (OS1Requests.deleteDestination did1)
`shouldBe` bhRequestMethod (OS3Requests.deleteDestination did3)
it "updateMonitorWith emits the same query params across backends" $ do
sortOn fst (getRawEndpointQueries (bhRequestEndpoint (OS1Requests.updateMonitorWith mid1 os1SampleMonitor (Just (7, 4)))))
`shouldBe` sortOn fst (getRawEndpointQueries (bhRequestEndpoint (OS3Requests.updateMonitorWith mid3 os3SampleMonitor (Just (7, 4)))))
-- =========================================================== --
-- Decoder tests for the new response/request types. --
-- =========================================================== --
describe "MonitorsSearchQuery rendering" $ do
it "default query renders to {}" $
encode OS3A.defaultMonitorsSearchQuery `shouldBe` "{}"
it "renders size and query and round-trips through encode/decode" $ do
let q =
OS3A.defaultMonitorsSearchQuery
{ OS3A.monitorsSearchQuerySize = Just 10,
OS3A.monitorsSearchQueryQuery =
Just
( object
[ "match"
.= object
["monitor.name" .= object ["query" .= ("my" :: Text)]]
]
)
}
-- aeson's KeyMap is hash-ordered, so compare by decoding rather than
-- asserting exact byte order.
decode (encode q) `shouldBe` Just q
describe "DestinationResponse decoding" $ do
let fixture =
LBS.pack
"{\"_id\":\"d1\",\"_version\":1,\"_seq_no\":0,\"_primary_term\":1,\"destination\":{\"id\":\"d1\",\"type\":\"slack\",\"name\":\"dest\",\"schema_version\":3,\"seq_no\":0,\"primary_term\":1,\"slack\":{\"url\":\"https://example.com\"}}}"
it "decodes the documented create-response fixture" $ do
let Just decoded = decode fixture :: Maybe OS3A.DestinationResponse
OS3A.destinationResponseId decoded `shouldBe` "d1"
OS3A.destinationResponseVersion decoded `shouldBe` 1
OS3A.destinationResponseSeqNo decoded `shouldBe` 0
OS3A.destinationResponsePrimaryTerm decoded `shouldBe` 1
let d = OS3A.destinationResponseDestination decoded
OS3A.destinationType d `shouldBe` OS3A.DestinationTypeSlack
OS3A.destinationName d `shouldBe` "dest"
it "round-trips through encode/decode on the typed fields" $ do
let original =
OS3A.DestinationResponse
{ OS3A.destinationResponseId = "d1",
OS3A.destinationResponseVersion = 1,
OS3A.destinationResponseSeqNo = 0,
OS3A.destinationResponsePrimaryTerm = 1,
OS3A.destinationResponseDestination = os3SampleSlackDestination
}
Just reDecoded = decode (encode original) :: Maybe OS3A.DestinationResponse
OS3A.destinationResponseId reDecoded `shouldBe` "d1"
OS3A.destinationResponseVersion reDecoded `shouldBe` 1
describe "DeleteDestinationResponse decoding" $ do
let fixture =
LBS.pack
"{\"_index\":\".opendistro-allocator-config\",\"_id\":\"d1\",\"_version\":2,\"result\":\"deleted\",\"forced_refresh\":false,\"_shards\":{\"total\":2,\"successful\":1,\"skipped\":0,\"failed\":0},\"_seq_no\":1,\"_primary_term\":1}"
it "decodes the bare delete-document fixture" $ do
let Just decoded = decode fixture :: Maybe OS3A.DeleteDestinationResponse
OS3A.deleteDestinationResponseIndex decoded `shouldBe` ".opendistro-allocator-config"
OS3A.deleteDestinationResponseId decoded `shouldBe` "d1"
OS3A.deleteDestinationResponseVersion decoded `shouldBe` 2
OS3A.deleteDestinationResponseResult decoded `shouldBe` "deleted"
OS3A.deleteDestinationResponseForcedRefresh decoded `shouldBe` False
let sh = OS3A.deleteDestinationResponseShards decoded
OS3A.alertingShardsTotal sh `shouldBe` 2
OS3A.alertingShardsSuccessful sh `shouldBe` 1
OS3A.deleteDestinationResponseSeqNo decoded `shouldBe` 1
OS3A.deleteDestinationResponsePrimaryTerm decoded `shouldBe` 1
describe "SearchMonitorsResponse decoding" $ do
let fixture =
LBS.pack
"{\"took\":5,\"timed_out\":false,\"_shards\":{\"total\":1,\"successful\":1,\"skipped\":0,\"failed\":0},\"hits\":{\"total\":{\"value\":1,\"relation\":\"eq\"},\"max_score\":1.0,\"hits\":[{\"_index\":\".opendistro-allocator-config\",\"_id\":\"m1\",\"_version\":1,\"_seq_no\":0,\"_primary_term\":1,\"_score\":1.0,\"_source\":{\"name\":\"my-monitor\",\"type\":\"monitor\",\"monitor_type\":\"query_level_monitor\",\"schedule\":{\"period\":{\"interval\":1,\"unit\":\"MINUTES\"}}}}]}}"
it "decodes the search envelope" $ do
let Just resp = decode fixture :: Maybe OS3A.SearchMonitorsResponse
OS3A.searchMonitorsResponseTook resp `shouldBe` 5
OS3A.searchMonitorsResponseTimedOut resp `shouldBe` False
OS3A.monitorsTotalValue (OS3A.searchMonitorsResponseTotal resp) `shouldBe` 1
OS3A.monitorsTotalRelation (OS3A.searchMonitorsResponseTotal resp)
`shouldBe` OS3A.MonitorsTotalRelationEq
OS3A.searchMonitorsResponseMaxScore resp `shouldBe` Just 1.0
length (OS3A.searchMonitorsResponseHits resp) `shouldBe` 1
it "searchMonitorsResponseMonitors projects out the bare Monitor list" $ do
let Just resp = decode fixture :: Maybe OS3A.SearchMonitorsResponse
case OS3A.searchMonitorsResponseMonitors resp of
(m : _) -> OS3A.monitorName m `shouldBe` "my-monitor"
[] -> expectationFailure "expected at least one monitor hit"
it "tolerates a missing max_score (defaults to Nothing)" $ do
let fixture' =
LBS.pack
"{\"took\":1,\"timed_out\":false,\"_shards\":{\"total\":1,\"successful\":1,\"skipped\":0,\"failed\":0},\"hits\":{\"total\":{\"value\":0,\"relation\":\"eq\"},\"hits\":[]}}"
let Just resp = decode fixture' :: Maybe OS3A.SearchMonitorsResponse
OS3A.searchMonitorsResponseMaxScore resp `shouldBe` Nothing
OS3A.searchMonitorsResponseHits resp `shouldBe` []
describe "ExecuteMonitorResponse decoding" $ do
let fixture =
LBS.pack
"{\"monitor_name\":\"my-monitor\",\"period_start\":1551466220455,\"period_end\":1551466280455,\"dry_run\":false,\"trigger_results\":{\"foo\":{\"action_id\":\"a1\"}},\"input_results\":{\"results\":[]},\"script_actions\":{}}"
it "decodes the typed shell and keeps the variable sub-objects opaque" $ do
let Just decoded = decode fixture :: Maybe OS3A.ExecuteMonitorResponse
OS3A.executeMonitorResponseMonitorName decoded `shouldBe` Just "my-monitor"
OS3A.executeMonitorResponsePeriodStart decoded `shouldBe` Just 1551466220455
OS3A.executeMonitorResponsePeriodEnd decoded `shouldBe` Just 1551466280455
OS3A.executeMonitorResponseDryRun decoded `shouldBe` Just False
OS3A.executeMonitorResponseTriggerResults decoded `shouldSatisfy` isJust
OS3A.executeMonitorResponseInputResults decoded `shouldSatisfy` isJust
it "round-trips the typed fields through encode/decode" $ do
let original =
OS3A.ExecuteMonitorResponse
{ OS3A.executeMonitorResponseMonitorName = Just "my-monitor",
OS3A.executeMonitorResponsePeriodStart = Just 1,
OS3A.executeMonitorResponsePeriodEnd = Just 2,
OS3A.executeMonitorResponseDryRun = Just True,
OS3A.executeMonitorResponseTriggerResults = Nothing,
OS3A.executeMonitorResponseInputResults = Nothing,
OS3A.executeMonitorResponseScriptActions = Nothing,
OS3A.executeMonitorResponseOther = Null
}
encoded = encode original
Just reDecoded = decode encoded :: Maybe OS3A.ExecuteMonitorResponse
OS3A.executeMonitorResponseMonitorName reDecoded `shouldBe` Just "my-monitor"
OS3A.executeMonitorResponseDryRun reDecoded `shouldBe` Just True
OS3A.executeMonitorResponsePeriodStart reDecoded `shouldBe` Just 1
describe "MonitorsTotalRelation discriminator" $ do
it "round-trips each documented value through monitorsTotalRelationText" $ do
OS3A.monitorsTotalRelationText OS3A.MonitorsTotalRelationEq `shouldBe` "eq"
OS3A.monitorsTotalRelationText OS3A.MonitorsTotalRelationGe `shouldBe` "ge"
it "falls through to MonitorsTotalRelationOther for an unknown value" $
decode "\"future\"" `shouldBe` Just (OS3A.MonitorsTotalRelationOther "future")
-- =========================================================== --
-- Get alerts endpoint shape + decoders. --
-- =========================================================== --
describe "getAlerts endpoint shape (OpenSearch 3)" $ do
it "getAlerts GETs /_plugins/_alerting/monitors/alerts with no body" $ do
let req = OS3Requests.getAlerts
bhRequestMethod req `shouldBe` "GET"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "monitors", "alerts"]
bhRequestBody req `shouldBe` Nothing
it "getAlertsWith with default options emits no query params" $ do
let req = OS3Requests.getAlertsWith OS3A.defaultGetAlertsOptions
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "getAlertsWith forwards options as camelCase query params (startIndex is camelCase, unlike destinations)" $ do
let opts =
OS3A.defaultGetAlertsOptions
{ OS3A.getAlertsOptionsSize = Just 5,
OS3A.getAlertsOptionsStartIndex = Just 10,
OS3A.getAlertsOptionsSortString = Just "monitor_name.keyword",
OS3A.getAlertsOptionsSortOrder = Just OS3A.AlertSortOrderDesc,
OS3A.getAlertsOptionsMissing = Just "_last",
OS3A.getAlertsOptionsSearchString = Just "alert",
OS3A.getAlertsOptionsSeverityLevel = Just "1",
OS3A.getAlertsOptionsAlertState = Just OS3A.AlertStateActive,
OS3A.getAlertsOptionsMonitorId = Just "mon-1",
OS3A.getAlertsOptionsWorkflowIds = Just "wf-1,wf-2"
}
let req = OS3Requests.getAlertsWith opts
sortOn fst (getRawEndpointQueries (bhRequestEndpoint req))
`shouldBe` sortOn
fst
[ ("alertState", Just "ACTIVE"),
("missing", Just "_last"),
("monitorId", Just "mon-1"),
("searchString", Just "alert"),
("severityLevel", Just "1"),
("size", Just "5"),
("sortOrder", Just "desc"),
("sortString", Just "monitor_name.keyword"),
("startIndex", Just "10"),
("workflowIds", Just "wf-1,wf-2")
]
it "getAlertsWith omits fields set to Nothing" $ do
let opts = OS3A.defaultGetAlertsOptions {OS3A.getAlertsOptionsSize = Just 5}
let req = OS3Requests.getAlertsWith opts
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` [("size", Just "5")]
describe "AlertState discriminator" $ do
it "round-trips each documented value through alertStateText" $ do
OS3A.alertStateText OS3A.AlertStateActive `shouldBe` "ACTIVE"
OS3A.alertStateText OS3A.AlertStateCompleted `shouldBe` "COMPLETED"
OS3A.alertStateText OS3A.AlertStateError `shouldBe` "ERROR"
OS3A.alertStateText OS3A.AlertStateAcknowledged `shouldBe` "ACKNOWLEDGED"
it "decodes each documented wire value" $ do
decode "\"ACTIVE\"" `shouldBe` Just OS3A.AlertStateActive
decode "\"COMPLETED\"" `shouldBe` Just OS3A.AlertStateCompleted
decode "\"ERROR\"" `shouldBe` Just OS3A.AlertStateError
decode "\"ACKNOWLEDGED\"" `shouldBe` Just OS3A.AlertStateAcknowledged
it "falls through to AlertStateOther for an unknown value" $
decode "\"DUMMY\"" `shouldBe` Just (OS3A.AlertStateOther "DUMMY")
describe "Alert decoding" $ do
let fixture =
LBS.pack
"{\"id\":\"eQURa3gBKo1jAh6qUo49\",\"version\":300,\"monitor_id\":\"awUMa3gBKo1jAh6qu47E\",\"schema_version\":2,\"monitor_version\":2,\"monitor_name\":\"Example_monitor_name\",\"trigger_id\":\"bQUQa3gBKo1jAh6qnY6G\",\"trigger_name\":\"Example_trigger_name\",\"state\":\"ACTIVE\",\"error_message\":null,\"severity\":\"1\",\"action_execution_results\":[],\"start_time\":1616704000492,\"last_notification_time\":1617317979908,\"end_time\":null,\"acknowledged_time\":null}"
it "decodes the documented alert fixture" $ do
let Just a = decode fixture :: Maybe OS3A.Alert
OS3A.alertId a `shouldBe` "eQURa3gBKo1jAh6qUo49"
OS3A.alertVersion a `shouldBe` 300
OS3A.alertMonitorId a `shouldBe` "awUMa3gBKo1jAh6qu47E"
OS3A.alertSchemaVersion a `shouldBe` Just 2
OS3A.alertMonitorVersion a `shouldBe` Just 2
OS3A.alertMonitorName a `shouldBe` "Example_monitor_name"
OS3A.alertTriggerId a `shouldBe` "bQUQa3gBKo1jAh6qnY6G"
OS3A.alertTriggerName a `shouldBe` "Example_trigger_name"
OS3A.alertState a `shouldBe` OS3A.AlertStateActive
OS3A.alertSeverity a `shouldBe` "1"
OS3A.alertErrorMessage a `shouldBe` Nothing
OS3A.alertStartTime a `shouldBe` Just 1616704000492
OS3A.alertLastNotificationTime a `shouldBe` Just 1617317979908
OS3A.alertEndTime a `shouldBe` Nothing
OS3A.alertAcknowledgedTime a `shouldBe` Nothing
it "keeps the variable sub-objects captured (action_execution_results round-trips)" $ do
let Just a = decode fixture :: Maybe OS3A.Alert
encode a `shouldSatisfy` \bs -> "action_execution_results" `isInfixOf` LBS.unpack bs
describe "GetAlertsResponse envelope decoding" $ do
let fixture =
LBS.pack
"{\"alerts\":[{\"id\":\"a1\",\"version\":1,\"monitor_id\":\"m1\",\"monitor_name\":\"mn\",\"trigger_id\":\"t1\",\"trigger_name\":\"tn\",\"state\":\"ACTIVE\",\"severity\":\"1\"}],\"totalAlerts\":1}"
it "decodes the documented envelope" $ do
let Just resp = decode fixture :: Maybe OS3A.GetAlertsResponse
OS3A.getAlertsResponseTotalAlerts resp `shouldBe` 1
length (OS3A.getAlertsResponseAlerts resp) `shouldBe` 1
it "decodes an empty alerts list" $ do
let Just resp = decode "{\"alerts\":[],\"totalAlerts\":0}" :: Maybe OS3A.GetAlertsResponse
OS3A.getAlertsResponseTotalAlerts resp `shouldBe` 0
OS3A.getAlertsResponseAlerts resp `shouldBe` []
it "tolerates a missing totalAlerts field (defaults to 0)" $ do
let Just resp = decode "{\"alerts\":[]}" :: Maybe OS3A.GetAlertsResponse
OS3A.getAlertsResponseTotalAlerts resp `shouldBe` 0
-- =========================================================== --
-- Acknowledge alert endpoint shape + decoders. --
-- =========================================================== --
describe "acknowledgeAlert endpoint shape (OpenSearch 3)" $
it "POSTs to /_plugins/_alerting/monitors/{id}/_acknowledge/alerts with the alerts body" $ do
let req = OS3Requests.acknowledgeAlert (OS3A.MonitorId "my-monitor-id") ["a1", "a2"]
bhRequestMethod req `shouldBe` "POST"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "monitors", "my-monitor-id", "_acknowledge", "alerts"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
let Just body = bhRequestBody req
body `shouldBe` "{\"alerts\":[\"a1\",\"a2\"]}"
describe "AcknowledgeAlertRequest / AcknowledgeAlertResponse" $ do
it "default request renders to {\"alerts\":[]}" $
encode OS3A.defaultAcknowledgeAlertRequest `shouldBe` "{\"alerts\":[]}"
it "decodes the documented acknowledge response (success + failed)" $ do
let Just resp = decode "{\"success\":[\"a1\"],\"failed\":[\"a2\"]}" :: Maybe OS3A.AcknowledgeAlertResponse
OS3A.acknowledgeAlertResponseSuccess resp `shouldBe` ["a1"]
OS3A.acknowledgeAlertResponseFailed resp `shouldBe` ["a2"]
it "tolerates a missing failed array (defaults to [])" $ do
let Just resp = decode "{\"success\":[\"a1\"]}" :: Maybe OS3A.AcknowledgeAlertResponse
OS3A.acknowledgeAlertResponseFailed resp `shouldBe` []
-- =========================================================== --
-- Monitor stats endpoint shape + decoders. --
-- =========================================================== --
describe "getMonitorStats endpoint shape (OpenSearch 3)" $ do
it "GETs /_plugins/_alerting/stats with no args" $ do
let req = OS3Requests.getMonitorStats Nothing Nothing
bhRequestMethod req `shouldBe` "GET"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "stats"]
it "GETs /_plugins/_alerting/stats/{metric} with a metric only" $ do
let req = OS3Requests.getMonitorStats Nothing (Just "job_scheduled_metrics")
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "stats", "job_scheduled_metrics"]
it "GETs /_plugins/_alerting/{node-id}/stats with a node only" $ do
let req = OS3Requests.getMonitorStats (Just "node-1") Nothing
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "node-1", "stats"]
it "GETs /_plugins/_alerting/{node-id}/stats/{metric} with both" $ do
let req = OS3Requests.getMonitorStats (Just "node-1") (Just "job_scheduled_metrics")
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "node-1", "stats", "job_scheduled_metrics"]
describe "MonitorStats decoding" $ do
let fixture =
LBS.pack
"{\"_nodes\":{\"total\":9,\"successful\":9,\"failed\":0},\"cluster_name\":\"alerting\",\"plugins.scheduled_jobs.enabled\":true,\"scheduled_job_index_exists\":true,\"scheduled_job_index_status\":\"green\",\"nodes_on_schedule\":9,\"nodes_not_on_schedule\":0,\"nodes\":{\"id1\":{\"name\":\"n\"}}}"
it "decodes the typed shell and keeps nodes opaque" $ do
let Just s = decode fixture :: Maybe OS3A.MonitorStats
let Just nodes = OS3A.monitorStatsNodes s
OS3A.monitorStatsNodesCountTotal nodes `shouldBe` 9
OS3A.monitorStatsNodesCountSuccessful nodes `shouldBe` 9
OS3A.monitorStatsNodesCountFailed nodes `shouldBe` 0
OS3A.monitorStatsClusterName s `shouldBe` Just "alerting"
OS3A.monitorStatsScheduledJobsEnabled s `shouldBe` Just True
OS3A.monitorStatsScheduledJobIndexExists s `shouldBe` Just True
OS3A.monitorStatsScheduledJobIndexStatus s `shouldBe` Just "green"
OS3A.monitorStatsNodesOnSchedule s `shouldBe` Just 9
OS3A.monitorStatsNodesNotOnSchedule s `shouldBe` Just 0
encode s `shouldSatisfy` \bs -> "\"nodes\"" `isInfixOf` LBS.unpack bs
it "tolerates a stats body with only the nodes map" $ do
let Just s = decode "{\"nodes\":{}}" :: Maybe OS3A.MonitorStats
OS3A.monitorStatsNodes s `shouldBe` Nothing
OS3A.monitorStatsClusterName s `shouldBe` Nothing
-- =========================================================== --
-- Email account lifecycle endpoint shape + decoders. --
-- =========================================================== --
describe "email account lifecycle endpoint shape (OpenSearch 3)" $ do
it "createEmailAccount POSTs to .../email_accounts with the encoded body" $ do
let req = OS3Requests.createEmailAccount os3SampleEmailAccount
bhRequestMethod req `shouldBe` "POST"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "destinations", "email_accounts"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
bhRequestBody req `shouldSatisfy` isJust
it "getEmailAccount GETs .../email_accounts/{id}" $ do
let req = OS3Requests.getEmailAccount (OS3A.EmailAccountId "ea-1")
bhRequestMethod req `shouldBe` "GET"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "destinations", "email_accounts", "ea-1"]
bhRequestBody req `shouldBe` Nothing
it "updateEmailAccount PUTs .../email_accounts/{id}" $ do
let req = OS3Requests.updateEmailAccount (OS3A.EmailAccountId "ea-1") os3SampleEmailAccount
bhRequestMethod req `shouldBe` "PUT"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "destinations", "email_accounts", "ea-1"]
it "updateEmailAccountWith emits if_seq_no and if_primary_term when Just" $ do
let req = OS3Requests.updateEmailAccountWith (OS3A.EmailAccountId "ea-1") os3SampleEmailAccount (Just (3, 2))
sortOn fst (getRawEndpointQueries (bhRequestEndpoint req))
`shouldBe` [("if_primary_term", Just "2"), ("if_seq_no", Just "3")]
it "updateEmailAccountWith omits query params when Nothing" $ do
let req = OS3Requests.updateEmailAccountWith (OS3A.EmailAccountId "ea-1") os3SampleEmailAccount Nothing
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "deleteEmailAccount DELETEs .../email_accounts/{id} with no body" $ do
let req = OS3Requests.deleteEmailAccount (OS3A.EmailAccountId "ea-1")
bhRequestMethod req `shouldBe` "DELETE"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "destinations", "email_accounts", "ea-1"]
bhRequestBody req `shouldBe` Nothing
describe "EmailAccount decoding" $ do
it "decodes the documented email account fixture" $ do
let fixture =
LBS.pack
"{\"name\":\"example_account\",\"email\":\"example@email.com\",\"host\":\"smtp.email.com\",\"port\":465,\"method\":\"ssl\",\"schema_version\":2}"
let Just a = decode fixture :: Maybe OS3A.EmailAccount
OS3A.emailAccountName a `shouldBe` "example_account"
OS3A.emailAccountEmail a `shouldBe` "example@email.com"
OS3A.emailAccountHost a `shouldBe` "smtp.email.com"
OS3A.emailAccountPort a `shouldBe` 465
OS3A.emailAccountMethod a `shouldBe` "ssl"
OS3A.emailAccountSchemaVersion a `shouldBe` Just 2
it "round-trips through encode/decode on the typed fields" $ do
let encoded = encode os3SampleEmailAccount
Just reDecoded = decode encoded :: Maybe OS3A.EmailAccount
OS3A.emailAccountName reDecoded `shouldBe` OS3A.emailAccountName os3SampleEmailAccount
OS3A.emailAccountPort reDecoded `shouldBe` OS3A.emailAccountPort os3SampleEmailAccount
OS3A.emailAccountMethod reDecoded `shouldBe` OS3A.emailAccountMethod os3SampleEmailAccount
describe "EmailAccountResponse decoding" $
it "decodes the documented create-response fixture" $ do
let fixture =
LBS.pack
"{\"_id\":\"ea-id\",\"_version\":1,\"_seq_no\":7,\"_primary_term\":2,\"email_account\":{\"schema_version\":2,\"name\":\"example_account\",\"email\":\"example@email.com\",\"host\":\"smtp.email.com\",\"port\":465,\"method\":\"ssl\"}}"
let Just r = decode fixture :: Maybe OS3A.EmailAccountResponse
OS3A.emailAccountResponseId r `shouldBe` "ea-id"
OS3A.emailAccountResponseVersion r `shouldBe` 1
OS3A.emailAccountResponseSeqNo r `shouldBe` 7
OS3A.emailAccountResponsePrimaryTerm r `shouldBe` 2
let ea = OS3A.emailAccountResponseEmailAccount r
OS3A.emailAccountName ea `shouldBe` "example_account"
OS3A.emailAccountMethod ea `shouldBe` "ssl"
describe "DeleteEmailAccountResponse decoding" $
it "decodes the bare delete-document fixture" $ do
let fixture =
LBS.pack
"{\"_index\":\".opendistro-alerting-config\",\"_id\":\"ea-id\",\"_version\":1,\"result\":\"deleted\",\"forced_refresh\":true,\"_shards\":{\"total\":2,\"successful\":2,\"failed\":0},\"_seq_no\":12,\"_primary_term\":2}"
let Just r = decode fixture :: Maybe OS3A.DeleteEmailAccountResponse
OS3A.deleteEmailAccountResponseIndex r `shouldBe` ".opendistro-alerting-config"
OS3A.deleteEmailAccountResponseId r `shouldBe` "ea-id"
OS3A.deleteEmailAccountResponseResult r `shouldBe` "deleted"
OS3A.deleteEmailAccountResponseForcedRefresh r `shouldBe` True
OS3A.deleteEmailAccountResponseSeqNo r `shouldBe` 12
-- =========================================================== --
-- Email group lifecycle endpoint shape + decoders. --
-- =========================================================== --
describe "email group lifecycle endpoint shape (OpenSearch 3)" $ do
it "createEmailGroup POSTs to .../email_groups with the encoded body" $ do
let req = OS3Requests.createEmailGroup os3SampleEmailGroup
bhRequestMethod req `shouldBe` "POST"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "destinations", "email_groups"]
bhRequestBody req `shouldSatisfy` isJust
it "getEmailGroup GETs .../email_groups/{id}" $ do
let req = OS3Requests.getEmailGroup (OS3A.EmailGroupId "eg-1")
bhRequestMethod req `shouldBe` "GET"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "destinations", "email_groups", "eg-1"]
it "updateEmailGroup PUTs .../email_groups/{id}" $ do
let req = OS3Requests.updateEmailGroup (OS3A.EmailGroupId "eg-1") os3SampleEmailGroup
bhRequestMethod req `shouldBe` "PUT"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "destinations", "email_groups", "eg-1"]
it "updateEmailGroupWith emits if_seq_no and if_primary_term when Just" $ do
let req = OS3Requests.updateEmailGroupWith (OS3A.EmailGroupId "eg-1") os3SampleEmailGroup (Just (5, 3))
sortOn fst (getRawEndpointQueries (bhRequestEndpoint req))
`shouldBe` [("if_primary_term", Just "3"), ("if_seq_no", Just "5")]
it "updateEmailGroupWith omits query params when Nothing" $ do
let req = OS3Requests.updateEmailGroupWith (OS3A.EmailGroupId "eg-1") os3SampleEmailGroup Nothing
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "deleteEmailGroup DELETEs .../email_groups/{id} with no body" $ do
let req = OS3Requests.deleteEmailGroup (OS3A.EmailGroupId "eg-1")
bhRequestMethod req `shouldBe` "DELETE"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "destinations", "email_groups", "eg-1"]
bhRequestBody req `shouldBe` Nothing
describe "EmailGroup decoding" $ do
it "decodes the documented email group fixture" $ do
let fixture =
LBS.pack
"{\"name\":\"example_email_group\",\"emails\":[{\"email\":\"example@email.com\"}],\"schema_version\":2}"
let Just g = decode fixture :: Maybe OS3A.EmailGroup
OS3A.emailGroupName g `shouldBe` "example_email_group"
OS3A.emailGroupSchemaVersion g `shouldBe` Just 2
case OS3A.emailGroupEmails g of
(r : _) -> OS3A.emailGroupRecipientEmail r `shouldBe` "example@email.com"
[] -> expectationFailure "expected at least one recipient"
it "tolerates a missing emails array (defaults to [])" $ do
let Just g = decode "{\"name\":\"empty_group\"}" :: Maybe OS3A.EmailGroup
OS3A.emailGroupName g `shouldBe` "empty_group"
OS3A.emailGroupEmails g `shouldBe` []
it "round-trips through encode/decode on the typed fields" $ do
let encoded = encode os3SampleEmailGroup
Just reDecoded = decode encoded :: Maybe OS3A.EmailGroup
OS3A.emailGroupName reDecoded `shouldBe` OS3A.emailGroupName os3SampleEmailGroup
length (OS3A.emailGroupEmails reDecoded)
`shouldBe` length (OS3A.emailGroupEmails os3SampleEmailGroup)
describe "EmailGroupResponse decoding" $
it "decodes the documented create-response fixture" $ do
let fixture =
LBS.pack
"{\"_id\":\"eg-id\",\"_version\":1,\"_seq_no\":9,\"_primary_term\":2,\"email_group\":{\"schema_version\":2,\"name\":\"example_email_group\",\"emails\":[{\"email\":\"example@email.com\"}]}}"
let Just r = decode fixture :: Maybe OS3A.EmailGroupResponse
OS3A.emailGroupResponseId r `shouldBe` "eg-id"
OS3A.emailGroupResponseVersion r `shouldBe` 1
OS3A.emailGroupResponseSeqNo r `shouldBe` 9
let eg = OS3A.emailGroupResponseEmailGroup r
OS3A.emailGroupName eg `shouldBe` "example_email_group"
length (OS3A.emailGroupEmails eg) `shouldBe` 1
describe "DeleteEmailGroupResponse decoding" $
it "decodes the bare delete-document fixture" $ do
let fixture =
LBS.pack
"{\"_index\":\".opendistro-alerting-config\",\"_id\":\"eg-id\",\"_version\":1,\"result\":\"deleted\",\"forced_refresh\":true,\"_shards\":{\"total\":2,\"successful\":2,\"failed\":0},\"_seq_no\":11,\"_primary_term\":2}"
let Just r = decode fixture :: Maybe OS3A.DeleteEmailGroupResponse
OS3A.deleteEmailGroupResponseId r `shouldBe` "eg-id"
OS3A.deleteEmailGroupResponseResult r `shouldBe` "deleted"
OS3A.deleteEmailGroupResponseForcedRefresh r `shouldBe` True
-- =========================================================== --
-- Cross-backend parity for the new endpoints (OS1=OS2=OS3). --
-- =========================================================== --
describe "cross-backend parity (alerts/acknowledge/stats/email endpoints)" $ do
let mid1 = OS1A.MonitorId "shared-id"
mid2 = OS2A.MonitorId "shared-id"
mid3 = OS3A.MonitorId "shared-id"
ea1 = OS1A.EmailAccountId "shared-ea"
ea2 = OS2A.EmailAccountId "shared-ea"
ea3 = OS3A.EmailAccountId "shared-ea"
eg1 = OS1A.EmailGroupId "shared-eg"
eg2 = OS2A.EmailGroupId "shared-eg"
eg3 = OS3A.EmailGroupId "shared-eg"
it "getAlerts: OS1, OS2, OS3 produce the same path and method" $ do
getRawEndpoint (bhRequestEndpoint OS1Requests.getAlerts)
`shouldBe` getRawEndpoint (bhRequestEndpoint OS2Requests.getAlerts)
getRawEndpoint (bhRequestEndpoint OS2Requests.getAlerts)
`shouldBe` getRawEndpoint (bhRequestEndpoint OS3Requests.getAlerts)
bhRequestMethod OS1Requests.getAlerts
`shouldBe` bhRequestMethod OS3Requests.getAlerts
it "acknowledgeAlert: OS1, OS2, OS3 produce the same path and method" $ do
getRawEndpoint (bhRequestEndpoint (OS1Requests.acknowledgeAlert mid1 ["a"]))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS2Requests.acknowledgeAlert mid2 ["a"]))
getRawEndpoint (bhRequestEndpoint (OS2Requests.acknowledgeAlert mid2 ["a"]))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS3Requests.acknowledgeAlert mid3 ["a"]))
bhRequestMethod (OS1Requests.acknowledgeAlert mid1 ["a"])
`shouldBe` bhRequestMethod (OS3Requests.acknowledgeAlert mid3 ["a"])
it "getMonitorStats: OS1, OS2, OS3 produce the same path" $ do
getRawEndpoint (bhRequestEndpoint (OS1Requests.getMonitorStats Nothing Nothing))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS2Requests.getMonitorStats Nothing Nothing))
getRawEndpoint (bhRequestEndpoint (OS2Requests.getMonitorStats Nothing Nothing))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS3Requests.getMonitorStats Nothing Nothing))
it "getMonitorStats: OS1, OS2, OS3 produce the same path with node + metric" $ do
getRawEndpoint (bhRequestEndpoint (OS1Requests.getMonitorStats (Just "n") (Just "m")))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS2Requests.getMonitorStats (Just "n") (Just "m")))
getRawEndpoint (bhRequestEndpoint (OS2Requests.getMonitorStats (Just "n") (Just "m")))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS3Requests.getMonitorStats (Just "n") (Just "m")))
it "deleteEmailAccount: OS1, OS2, OS3 produce the same path and method" $ do
getRawEndpoint (bhRequestEndpoint (OS1Requests.deleteEmailAccount ea1))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS2Requests.deleteEmailAccount ea2))
getRawEndpoint (bhRequestEndpoint (OS2Requests.deleteEmailAccount ea2))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS3Requests.deleteEmailAccount ea3))
bhRequestMethod (OS1Requests.deleteEmailAccount ea1)
`shouldBe` bhRequestMethod (OS3Requests.deleteEmailAccount ea3)
it "deleteEmailGroup: OS1, OS2, OS3 produce the same path and method" $ do
getRawEndpoint (bhRequestEndpoint (OS1Requests.deleteEmailGroup eg1))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS2Requests.deleteEmailGroup eg2))
getRawEndpoint (bhRequestEndpoint (OS2Requests.deleteEmailGroup eg2))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS3Requests.deleteEmailGroup eg3))
bhRequestMethod (OS1Requests.deleteEmailGroup eg1)
`shouldBe` bhRequestMethod (OS3Requests.deleteEmailGroup eg3)
it "getAlertsWith emits identical query params across backends" $ do
let o1 =
OS1A.defaultGetAlertsOptions
{ OS1A.getAlertsOptionsSize = Just 5,
OS1A.getAlertsOptionsAlertState = Just OS1A.AlertStateActive
}
o3 =
OS3A.defaultGetAlertsOptions
{ OS3A.getAlertsOptionsSize = Just 5,
OS3A.getAlertsOptionsAlertState = Just OS3A.AlertStateActive
}
sortOn fst (getRawEndpointQueries (bhRequestEndpoint (OS1Requests.getAlertsWith o1)))
`shouldBe` sortOn fst (getRawEndpointQueries (bhRequestEndpoint (OS3Requests.getAlertsWith o3)))
-- =========================================================== --
-- New endpoint shape: getDestination, search endpoints, --
-- comments CRUD (bloodhound-ee2). --
-- =========================================================== --
describe "getDestination endpoint shape (OpenSearch 3)" $ do
it "getDestination GETs /_plugins/_alerting/destinations/{id} with no body" $ do
let req = OS3Requests.getDestination (OS3A.DestinationId "my-dest")
bhRequestMethod req `shouldBe` "GET"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "destinations", "my-dest"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
bhRequestBody req `shouldBe` Nothing
it "getDestination keeps the id as a single opaque path segment" $ do
let req = OS3Requests.getDestination (OS3A.DestinationId "dest_42")
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "destinations", "dest_42"]
describe "searchEmailAccounts endpoint shape (OpenSearch 3)" $ do
it "searchEmailAccounts POSTs to .../email_accounts/_search with a body" $ do
let req = OS3Requests.searchEmailAccounts Nothing
bhRequestMethod req `shouldBe` "POST"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "destinations", "email_accounts", "_search"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
bhRequestBody req `shouldSatisfy` isJust
it "searchEmailAccounts with Nothing emits an empty-body {} JSON" $ do
let req = OS3Requests.searchEmailAccounts Nothing
let body = fromJust (bhRequestBody req)
decode body `shouldBe` Just (Object mempty)
it "searchEmailAccounts with Just encodes the query DSL" $ do
let q =
OS3A.EmailAccountSearchQuery
{ OS3A.emailAccountSearchQueryFrom = Just 0,
OS3A.emailAccountSearchQuerySize = Just 10,
OS3A.emailAccountSearchQuerySort = Nothing,
OS3A.emailAccountSearchQueryQuery = Nothing
}
let req = OS3Requests.searchEmailAccounts (Just q)
let body = fromJust (bhRequestBody req)
decode body `shouldBe` Just (object ["from" .= (0 :: Int), "size" .= (10 :: Int)])
describe "searchEmailGroups endpoint shape (OpenSearch 3)" $ do
it "searchEmailGroups POSTs to .../email_groups/_search with a body" $ do
let req = OS3Requests.searchEmailGroups Nothing
bhRequestMethod req `shouldBe` "POST"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "destinations", "email_groups", "_search"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
bhRequestBody req `shouldSatisfy` isJust
describe "searchFindings endpoint shape (OpenSearch 3)" $ do
it "searchFindings GETs /_plugins/_alerting/findings/_search with no body" $ do
let req = OS3Requests.searchFindings Nothing
bhRequestMethod req `shouldBe` "GET"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "findings", "_search"]
bhRequestBody req `shouldBe` Nothing
it "searchFindings with default options emits no query params" $ do
let req = OS3Requests.searchFindings Nothing
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "searchFindings with options forwards them as query params" $ do
let opts =
OS3A.defaultFindingsSearchOptions
{ OS3A.findingsSearchOptionsFindingId = Just "finding-1",
OS3A.findingsSearchOptionsSize = Just 20
}
let req = OS3Requests.searchFindings (Just opts)
getRawEndpointQueries (bhRequestEndpoint req)
`shouldSatisfy` any (\(k, v) -> k == "findingId" && v == Just "finding-1")
getRawEndpointQueries (bhRequestEndpoint req)
`shouldSatisfy` any (\(k, v) -> k == "size" && v == Just "20")
describe "createComment endpoint shape (OpenSearch 3)" $ do
it "createComment POSTs to /_plugins/_alerting/comments/{alertId} with a body" $ do
let req = OS3Requests.createComment "alert-1" (OS3A.CreateCommentRequest "hello")
bhRequestMethod req `shouldBe` "POST"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "comments", "alert-1"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
let body = fromJust (bhRequestBody req)
decode body `shouldBe` Just (object ["content" .= ("hello" :: Text)])
describe "updateComment endpoint shape (OpenSearch 3)" $ do
it "updateComment PUTs to /_plugins/_alerting/comments/{commentId} with a body" $ do
let req = OS3Requests.updateComment (OS3A.CommentId "c-1") (OS3A.UpdateCommentRequest "updated")
bhRequestMethod req `shouldBe` "PUT"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "comments", "c-1"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
let body = fromJust (bhRequestBody req)
decode body `shouldBe` Just (object ["content" .= ("updated" :: Text)])
describe "searchComments endpoint shape (OpenSearch 3)" $ do
it "searchComments GETs /_plugins/_alerting/comments/_search with a body" $ do
let req = OS3Requests.searchComments Nothing
bhRequestMethod req `shouldBe` "GET"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "comments", "_search"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
bhRequestBody req `shouldSatisfy` isJust
it "searchComments with Nothing emits an empty-body {} JSON" $ do
let req = OS3Requests.searchComments Nothing
let body = fromJust (bhRequestBody req)
decode body `shouldBe` Just (Object mempty)
describe "deleteComment endpoint shape (OpenSearch 3)" $ do
it "deleteComment DELETEs /_plugins/_alerting/comments/{commentId} with no body" $ do
let req = OS3Requests.deleteComment (OS3A.CommentId "c-1")
bhRequestMethod req `shouldBe` "DELETE"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_alerting", "comments", "c-1"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
bhRequestBody req `shouldBe` Nothing
-- =========================================================== --
-- Cross-backend parity for the new endpoints (ee2). --
-- =========================================================== --
describe "cross-backend parity (new endpoints)" $ do
let did1 = OS1A.DestinationId "shared-dest"
did2 = OS2A.DestinationId "shared-dest"
did3 = OS3A.DestinationId "shared-dest"
cid2 = OS2A.CommentId "shared-comment"
cid3 = OS3A.CommentId "shared-comment"
it "getDestination: OS1, OS2, OS3 produce the same path and method" $ do
getRawEndpoint (bhRequestEndpoint (OS1Requests.getDestination did1))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS2Requests.getDestination did2))
getRawEndpoint (bhRequestEndpoint (OS2Requests.getDestination did2))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS3Requests.getDestination did3))
bhRequestMethod (OS1Requests.getDestination did1)
`shouldBe` bhRequestMethod (OS3Requests.getDestination did3)
it "searchEmailAccounts: OS1, OS2, OS3 produce the same path and method" $ do
getRawEndpoint (bhRequestEndpoint (OS1Requests.searchEmailAccounts Nothing))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS2Requests.searchEmailAccounts Nothing))
getRawEndpoint (bhRequestEndpoint (OS2Requests.searchEmailAccounts Nothing))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS3Requests.searchEmailAccounts Nothing))
bhRequestMethod (OS1Requests.searchEmailAccounts Nothing)
`shouldBe` bhRequestMethod (OS3Requests.searchEmailAccounts Nothing)
it "searchEmailGroups: OS1, OS2, OS3 produce the same path and method" $ do
getRawEndpoint (bhRequestEndpoint (OS1Requests.searchEmailGroups Nothing))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS2Requests.searchEmailGroups Nothing))
getRawEndpoint (bhRequestEndpoint (OS2Requests.searchEmailGroups Nothing))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS3Requests.searchEmailGroups Nothing))
bhRequestMethod (OS1Requests.searchEmailGroups Nothing)
`shouldBe` bhRequestMethod (OS3Requests.searchEmailGroups Nothing)
it "searchFindings: OS2 and OS3 produce the same path and method" $ do
getRawEndpoint (bhRequestEndpoint (OS2Requests.searchFindings Nothing))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS3Requests.searchFindings Nothing))
bhRequestMethod (OS2Requests.searchFindings Nothing)
`shouldBe` bhRequestMethod (OS3Requests.searchFindings Nothing)
it "createComment: OS2 and OS3 produce the same path and method" $ do
getRawEndpoint (bhRequestEndpoint (OS2Requests.createComment "x" (OS2A.CreateCommentRequest "c")))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS3Requests.createComment "x" (OS3A.CreateCommentRequest "c")))
bhRequestMethod (OS2Requests.createComment "x" (OS2A.CreateCommentRequest "c"))
`shouldBe` bhRequestMethod (OS3Requests.createComment "x" (OS3A.CreateCommentRequest "c"))
it "updateComment: OS2 and OS3 produce the same path and method" $ do
getRawEndpoint (bhRequestEndpoint (OS2Requests.updateComment cid2 (OS2A.UpdateCommentRequest "c")))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS3Requests.updateComment cid3 (OS3A.UpdateCommentRequest "c")))
bhRequestMethod (OS2Requests.updateComment cid2 (OS2A.UpdateCommentRequest "c"))
`shouldBe` bhRequestMethod (OS3Requests.updateComment cid3 (OS3A.UpdateCommentRequest "c"))
it "searchComments: OS2 and OS3 produce the same path and method" $ do
getRawEndpoint (bhRequestEndpoint (OS2Requests.searchComments Nothing))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS3Requests.searchComments Nothing))
bhRequestMethod (OS2Requests.searchComments Nothing)
`shouldBe` bhRequestMethod (OS3Requests.searchComments Nothing)
it "deleteComment: OS2 and OS3 produce the same path and method" $ do
getRawEndpoint (bhRequestEndpoint (OS2Requests.deleteComment cid2))
`shouldBe` getRawEndpoint (bhRequestEndpoint (OS3Requests.deleteComment cid3))
bhRequestMethod (OS2Requests.deleteComment cid2)
`shouldBe` bhRequestMethod (OS3Requests.deleteComment cid3)
-- =========================================================== --
-- Decoder tests for the new types (ee2). --
-- =========================================================== --
describe "SearchEmailAccountsResponse decoding" $ do
it "decodes a documented search response fixture" $ do
let wire =
LBS.pack
"{\"took\":5,\"timed_out\":false,\"_shards\":{\"total\":1,\"successful\":1,\"skipped\":0,\"failed\":0},\"hits\":{\"total\":{\"value\":1,\"relation\":\"eq\"},\"max_score\":1.0,\"hits\":[{\"_index\":\".opendistro-alerting-config\",\"_id\":\"email_account_1\",\"_version\":1,\"_seq_no\":0,\"_primary_term\":1,\"_score\":1.0,\"_source\":{\"name\":\"example_account\",\"email\":\"example@email.com\",\"host\":\"smtp.email.com\",\"port\":465,\"method\":\"ssl\"}}]}}"
let decoded = decode wire :: Maybe OS3A.SearchEmailAccountsResponse
decoded `shouldSatisfy` isJust
let resp = fromJust decoded
OS3A.searchEmailAccountsResponseTook resp `shouldBe` 5
OS3A.searchEmailAccountsResponseTotal resp
`shouldBe` OS3A.MonitorsTotal {OS3A.monitorsTotalValue = 1, OS3A.monitorsTotalRelation = OS3A.MonitorsTotalRelationEq}
length (OS3A.searchEmailAccountsResponseHits resp) `shouldBe` 1
let hit = head (OS3A.searchEmailAccountsResponseHits resp)
OS3A.emailAccountSearchHitId hit `shouldBe` "email_account_1"
OS3A.emailAccountName (OS3A.emailAccountSearchHitSource hit) `shouldBe` "example_account"
describe "SearchEmailGroupsResponse decoding" $ do
it "decodes a documented search response fixture" $ do
let wire =
LBS.pack
"{\"took\":3,\"timed_out\":false,\"_shards\":{\"total\":1,\"successful\":1,\"skipped\":0,\"failed\":0},\"hits\":{\"total\":{\"value\":1,\"relation\":\"eq\"},\"max_score\":1.0,\"hits\":[{\"_index\":\".opendistro-alerting-config\",\"_id\":\"email_group_1\",\"_score\":1.0,\"_source\":{\"name\":\"example_email_group\",\"emails\":[{\"email\":\"example@email.com\"}]}}]}}"
let decoded = decode wire :: Maybe OS3A.SearchEmailGroupsResponse
decoded `shouldSatisfy` isJust
let resp = fromJust decoded
OS3A.searchEmailGroupsResponseTook resp `shouldBe` 3
length (OS3A.searchEmailGroupsResponseHits resp) `shouldBe` 1
let hit = head (OS3A.searchEmailGroupsResponseHits resp)
OS3A.emailGroupSearchHitId hit `shouldBe` "email_group_1"
OS3A.emailGroupName (OS3A.emailGroupSearchHitSource hit) `shouldBe` "example_email_group"
describe "SearchFindingsResponse decoding" $ do
it "decodes a response with total_findings and opaque findings list" $ do
let wire =
LBS.pack
"{\"total_findings\":2,\"findings\":[{\"id\":\"f-1\",\"related_doc_ids\":[\"doc-1\"]},{\"id\":\"f-2\",\"related_doc_ids\":[\"doc-2\"]}]}"
let decoded = decode wire :: Maybe OS3A.SearchFindingsResponse
decoded `shouldSatisfy` isJust
let resp = fromJust decoded
OS3A.searchFindingsResponseTotalFindings resp `shouldBe` 2
it "decodes a response with missing total_findings (defaults to 0)" $ do
let wire = LBS.pack "{\"findings\":[]}"
let decoded = decode wire :: Maybe OS3A.SearchFindingsResponse
decoded `shouldSatisfy` isJust
OS3A.searchFindingsResponseTotalFindings (fromJust decoded) `shouldBe` 0
describe "Comment decoding" $ do
it "decodes a full comment fixture" $ do
let wire =
LBS.pack
"{\"entity_id\":\"alert-1\",\"entity_type\":\"alert\",\"content\":\"This is a comment\",\"created_time\":1603943261722,\"last_updated_time\":1603943261723,\"user\":\"admin\"}"
let decoded = decode wire :: Maybe OS3A.Comment
decoded `shouldSatisfy` isJust
let c = fromJust decoded
OS3A.commentEntityId c `shouldBe` "alert-1"
OS3A.commentEntityType c `shouldBe` "alert"
OS3A.commentContent c `shouldBe` "This is a comment"
OS3A.commentCreatedTime c `shouldBe` Just 1603943261722
OS3A.commentLastUpdatedTime c `shouldBe` Just 1603943261723
OS3A.commentUser c `shouldBe` Just "admin"
it "round-trips through encode/decode losslessly on typed fields" $ do
let c =
OS3A.Comment
{ OS3A.commentEntityId = "alert-1",
OS3A.commentEntityType = "alert",
OS3A.commentContent = "hello",
OS3A.commentCreatedTime = Just 100,
OS3A.commentLastUpdatedTime = Nothing,
OS3A.commentUser = Nothing,
OS3A.commentOther = Null
}
let encoded = encode c
let decoded = decode encoded :: Maybe OS3A.Comment
decoded `shouldSatisfy` isJust
OS3A.commentContent (fromJust decoded) `shouldBe` "hello"
OS3A.commentEntityId (fromJust decoded) `shouldBe` "alert-1"
describe "CommentResponse decoding" $ do
it "decodes a create-comment response fixture" $ do
let wire =
LBS.pack
"{\"_id\":\"comment-1\",\"_seq_no\":0,\"_primary_term\":1,\"comment\":{\"entity_id\":\"alert-1\",\"entity_type\":\"alert\",\"content\":\"This is a comment\"}}"
let decoded = decode wire :: Maybe OS3A.CommentResponse
decoded `shouldSatisfy` isJust
let resp = fromJust decoded
OS3A.commentResponseId resp `shouldBe` "comment-1"
OS3A.commentResponseSeqNo resp `shouldBe` 0
OS3A.commentResponsePrimaryTerm resp `shouldBe` 1
OS3A.commentContent (OS3A.commentResponseComment resp) `shouldBe` "This is a comment"
describe "DeleteCommentResponse decoding" $ do
it "decodes the bare {_id: ...} fixture" $ do
let wire = LBS.pack "{\"_id\":\"comment-1\"}"
let decoded = decode wire :: Maybe OS3A.DeleteCommentResponse
decoded `shouldSatisfy` isJust
OS3A.deleteCommentResponseId (fromJust decoded) `shouldBe` "comment-1"
describe "SearchCommentsResponse decoding" $ do
it "decodes a response with a comment hit" $ do
let wire =
LBS.pack
"{\"took\":2,\"timed_out\":false,\"_shards\":{\"total\":1,\"successful\":1,\"skipped\":0,\"failed\":0},\"hits\":{\"total\":{\"value\":1,\"relation\":\"eq\"},\"max_score\":1.0,\"hits\":[{\"_index\":\".opensearch-alerting-comments-history-2024\",\"_id\":\"comment-1\",\"_score\":1.0,\"_source\":{\"entity_id\":\"alert-1\",\"entity_type\":\"alert\",\"content\":\"A comment\",\"created_time\":1603943261722,\"last_updated_time\":1603943261723,\"user\":\"admin\"}}]}}"
let decoded = decode wire :: Maybe OS3A.SearchCommentsResponse
decoded `shouldSatisfy` isJust
let resp = fromJust decoded
OS3A.searchCommentsResponseTook resp `shouldBe` 2
length (OS3A.searchCommentsResponseHits resp) `shouldBe` 1
let hit = head (OS3A.searchCommentsResponseHits resp)
OS3A.commentSearchHitId hit `shouldBe` "comment-1"
OS3A.commentContent (OS3A.commentSearchHitSource hit) `shouldBe` "A comment"
describe "CreateCommentRequest encoding" $
it "encodes content as {\"content\": \"...\"}" $ do
let req = OS3A.CreateCommentRequest "my comment"
encode req `shouldBe` LBS.pack "{\"content\":\"my comment\"}"
describe "UpdateCommentRequest encoding" $
it "encodes content as {\"content\": \"...\"}" $ do
let req = OS3A.UpdateCommentRequest "updated text"
encode req `shouldBe` LBS.pack "{\"content\":\"updated text\"}"
describe "FindingsSearchOptions query params" $ do
it "defaultFindingsSearchOptions renders no params" $ do
OS3A.findingsSearchOptionsParams OS3A.defaultFindingsSearchOptions `shouldBe` []
it "renders findingId, size, sortOrder as query params" $ do
let opts =
OS3A.defaultFindingsSearchOptions
{ OS3A.findingsSearchOptionsFindingId = Just "f-1",
OS3A.findingsSearchOptionsSize = Just 50,
OS3A.findingsSearchOptionsSortOrder = Just OS3A.DestinationSortOrderDesc
}
let params = OS3A.findingsSearchOptionsParams opts
lookup "findingId" params `shouldBe` Just (Just "f-1")
lookup "size" params `shouldBe` Just (Just "50")
lookup "sortOrder" params `shouldBe` Just (Just "desc")
-- =========================================================== --
-- Live round-trip: gated per-OS with os{n}OnlyIT (the --
-- 'Test.NeuralClearCacheSpec' pattern). Wire shapes --
-- live-verified on OS 1.3.19, 2.19.5 and 3.7.0 (bead --
-- bloodhound-z5j). --
-- --
-- The DELETE-monitor response shape is version-dependent: OS --
-- 1.3.x returns the full bare ES delete-document body (with --
-- @_index@, @_shards@, @_seq_no@, ...); OS 2.x/3.x return only --
-- @{_id,_version}@. The per-OS assertions pin that contract so --
-- a regression to the old over-strict decoder (which failed on --
-- OS 2.x/3.x) is caught immediately. --
-- =========================================================== --
describe "deleteMonitor live round-trip (version-dependent shape)" $ do
os1It <- runIO os1OnlyIT
os2It <- runIO os2OnlyIT
os3It <- runIO os3OnlyIT
os1It "OS1: create -> delete returns the full ES delete-document body" $
withTestEnv $ do
resp <- OS1.Client.createMonitor os1SampleMonitor
let mid = OS1A.MonitorId (OS1A.monitorResponseId resp)
del <- OS1.Client.deleteMonitor mid
liftIO $ do
OS1A.deleteMonitorResponseId del `shouldBe` OS1A.monitorResponseId resp
OS1A.deleteMonitorResponseVersion del `shouldSatisfy` (>= 1)
OS1A.deleteMonitorResponseIndex del `shouldSatisfy` isJust
OS1A.deleteMonitorResponseShards del `shouldSatisfy` isJust
OS1A.deleteMonitorResponseResult del `shouldBe` Just "deleted"
os2It "OS2: create -> delete returns the minimal {_id,_version} body" $
withTestEnv $ do
resp <- OS2.Client.createMonitor os2SampleMonitor
let mid = OS2A.MonitorId (OS2A.monitorResponseId resp)
del <- OS2.Client.deleteMonitor mid
liftIO $ do
OS2A.deleteMonitorResponseId del `shouldBe` OS2A.monitorResponseId resp
OS2A.deleteMonitorResponseVersion del `shouldSatisfy` (>= 1)
OS2A.deleteMonitorResponseIndex del `shouldBe` Nothing
OS2A.deleteMonitorResponseShards del `shouldBe` Nothing
OS2A.deleteMonitorResponseResult del `shouldBe` Nothing
os3It "OS3: create -> delete returns the minimal {_id,_version} body" $
withTestEnv $ do
resp <- OS3.Client.createMonitor os3SampleMonitor
let mid = OS3A.MonitorId (OS3A.monitorResponseId resp)
del <- OS3.Client.deleteMonitor mid
liftIO $ do
OS3A.deleteMonitorResponseId del `shouldBe` OS3A.monitorResponseId resp
OS3A.deleteMonitorResponseVersion del `shouldSatisfy` (>= 1)
OS3A.deleteMonitorResponseIndex del `shouldBe` Nothing
OS3A.deleteMonitorResponseShards del `shouldBe` Nothing
OS3A.deleteMonitorResponseResult del `shouldBe` Nothing
-- ---------------------------------------------------------------- --
-- Sample fixtures (structurally identical across OS1/OS2/OS3; --
-- nominally distinct types, so one per backend). --
-- ---------------------------------------------------------------- --
os3SampleMonitor :: OS3A.Monitor
os3SampleMonitor =
OS3A.Monitor
{ OS3A.monitorName = "test-monitor",
OS3A.monitorType = Just OS3A.AlertTypeMonitor,
OS3A.monitorMonitorType = Just OS3A.MonitorTypeQueryLevel,
OS3A.monitorEnabled = Just True,
OS3A.monitorEnabledTime = Nothing,
OS3A.monitorLastUpdateTime = Nothing,
OS3A.monitorSchemaVersion = Nothing,
OS3A.monitorSchedule = OS3A.PeriodSchedule (OS3A.SchedulePeriod 1 "MINUTES"),
OS3A.monitorInputs = [],
OS3A.monitorTriggers = [],
OS3A.monitorUser = Nothing,
OS3A.monitorRbacRoles = Nothing,
OS3A.monitorOther = Null
}
os1SampleMonitor :: OS1A.Monitor
os1SampleMonitor =
OS1A.Monitor
{ OS1A.monitorName = "test-monitor",
OS1A.monitorType = Just OS1A.AlertTypeMonitor,
OS1A.monitorMonitorType = Just OS1A.MonitorTypeQueryLevel,
OS1A.monitorEnabled = Just True,
OS1A.monitorEnabledTime = Nothing,
OS1A.monitorLastUpdateTime = Nothing,
OS1A.monitorSchemaVersion = Nothing,
OS1A.monitorSchedule = OS1A.PeriodSchedule (OS1A.SchedulePeriod 1 "MINUTES"),
OS1A.monitorInputs = [],
OS1A.monitorTriggers = [],
OS1A.monitorUser = Nothing,
OS1A.monitorRbacRoles = Nothing,
OS1A.monitorOther = Null
}
os2SampleMonitor :: OS2A.Monitor
os2SampleMonitor =
OS2A.Monitor
{ OS2A.monitorName = "test-monitor",
OS2A.monitorType = Just OS2A.AlertTypeMonitor,
OS2A.monitorMonitorType = Just OS2A.MonitorTypeQueryLevel,
OS2A.monitorEnabled = Just True,
OS2A.monitorEnabledTime = Nothing,
OS2A.monitorLastUpdateTime = Nothing,
OS2A.monitorSchemaVersion = Nothing,
OS2A.monitorSchedule = OS2A.PeriodSchedule (OS2A.SchedulePeriod 1 "MINUTES"),
OS2A.monitorInputs = [],
OS2A.monitorTriggers = [],
OS2A.monitorUser = Nothing,
OS2A.monitorRbacRoles = Nothing,
OS2A.monitorOther = Null
}
-- | Helper assertion: the given wire string decodes to the expected
-- 'MonitorType'.
decodesMonitorTypeTo :: Text -> OS3A.MonitorType -> Expectation
decodesMonitorTypeTo wire expected =
decode (encode wire) `shouldBe` Just expected
-- | Helper assertion: the given wire string decodes to the expected
-- 'DestinationType'.
decodesDestinationTypeTo :: Text -> OS3A.DestinationType -> Expectation
decodesDestinationTypeTo wire expected =
decode (encode wire) `shouldBe` Just expected
-- | Sample 'Destination' for round-trip tests. Mirrors the structure
-- of the documented Slack fixture but is built as a Haskell value so
-- the encode direction is also exercised.
os3SampleSlackDestination :: OS3A.Destination
os3SampleSlackDestination =
OS3A.Destination
{ OS3A.destinationId = "sample-id",
OS3A.destinationType = OS3A.DestinationTypeSlack,
OS3A.destinationName = "sample-destination",
OS3A.destinationSchemaVersion = 3,
OS3A.destinationSeqNo = 0,
OS3A.destinationPrimaryTerm = 6,
OS3A.destinationLastUpdateTime = Just 1603943261722,
OS3A.destinationUser = Nothing,
OS3A.destinationBody = object ["url" .= ("https://example.com" :: Text)],
OS3A.destinationOther = Null
}
-- | Sample 'EmailAccount' for round-trip / endpoint-shape tests.
os3SampleEmailAccount :: OS3A.EmailAccount
os3SampleEmailAccount =
OS3A.EmailAccount
{ OS3A.emailAccountName = "example_account",
OS3A.emailAccountEmail = "example@email.com",
OS3A.emailAccountHost = "smtp.email.com",
OS3A.emailAccountPort = 465,
OS3A.emailAccountMethod = "ssl",
OS3A.emailAccountSchemaVersion = Just 2,
OS3A.emailAccountOther = Null
}
-- | Sample 'EmailGroup' for round-trip / endpoint-shape tests.
os3SampleEmailGroup :: OS3A.EmailGroup
os3SampleEmailGroup =
OS3A.EmailGroup
{ OS3A.emailGroupName = "example_email_group",
OS3A.emailGroupEmails =
[ OS3A.EmailGroupRecipient
{ OS3A.emailGroupRecipientEmail = "example@email.com",
OS3A.emailGroupRecipientOther = Null
}
],
OS3A.emailGroupSchemaVersion = Just 2,
OS3A.emailGroupOther = Null
}