bloodhound-1.0.0.0: tests/Test/SQLPPLSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module Test.SQLPPLSpec (spec) where
import Data.ByteString.Char8 qualified as BS
import Data.ByteString.Lazy.Char8 qualified as LBS
import Data.List qualified as L
import Database.Bloodhound.OpenSearch1.Requests qualified as OS1Requests
import Database.Bloodhound.OpenSearch1.Types qualified as OS1Types
import Database.Bloodhound.OpenSearch2.Requests qualified as OS2Requests
import Database.Bloodhound.OpenSearch2.Types qualified as OS2Types
import Database.Bloodhound.OpenSearch3.Requests qualified as OS3Requests
import Database.Bloodhound.OpenSearch3.Types
import TestsUtils.Import
import Prelude
-- ---------------------------------------------------------------------------
-- Sample bodies, drawn verbatim from the OS SQL and PPL API documentation
-- (<https://docs.opensearch.org/latest/sql-and-ppl/sql-and-ppl-api/index/>).
-- ---------------------------------------------------------------------------
-- | A representative first-page response from @POST /_plugins/_sql@, with
-- every field populated. Truncated @schema@ for brevity; the wire shape is
-- what matters.
sampleFirstPage :: LBS.ByteString
sampleFirstPage =
"{\
\ \"schema\": [\
\ {\"name\": \"firstname\", \"type\": \"text\"},\
\ {\"name\": \"lastname\", \"type\": \"text\"}\
\ ],\
\ \"datarows\": [\
\ [\"Cherry\", \"Carey\"],\
\ [\"Lindsey\", \"Hawkins\"]\
\ ],\
\ \"total\": 956,\
\ \"size\": 2,\
\ \"status\": 200\
\}"
-- | A paginated first-page response with @fetch_size@ set: carries a
-- 'sqlResultCursor' in addition to the standard fields.
samplePaginatedFirstPage :: LBS.ByteString
samplePaginatedFirstPage =
"{\
\ \"schema\": [{\"name\": \"firstname\", \"type\": \"text\"}],\
\ \"cursor\": \"d:eyJhIjp7fSwicyI6IkRYRjFaWEo1UVc1a1JtVjBZMmdCQUFBQUFBQUFBQU1XZWpkdFRFRkZUMlpTZEZkeFdsWnJkRlZoYnpaeVVRPT0iLCJjIjpbeyJuYW1lIjoiZmlyc3RuYW1lIiwidHlwZSI6InRleHQifV0sImYiOjUsImkiOiJhY2NvdW50cyIsImwiOjk1MX0\",\
\ \"total\": 956,\
\ \"datarows\": [[\"Cherry\"], [\"Lindsey\"], [\"Sargent\"], [\"Campos\"], [\"Savannah\"]],\
\ \"size\": 5,\
\ \"status\": 200\
\}"
-- | A continuation response (cursor followed up via 'sqlQueryNextPage').
-- Per the plugin spec, only @datarows@ and a new @cursor@ are present.
sampleContinuationPage :: LBS.ByteString
sampleContinuationPage =
"{\
\ \"cursor\": \"d:eyJhIjp7fSwicyI6IkRYRjFaWEo1UVc1a1JtVjBZMmdCQUFBQUFBQUFBQU1XZWpkdFRFRkZUMlpTZEZkeFdsWnJkRlZoYnpaeVVRPT0iLCJjIjpbeyJuYW1lIjoiZmlyc3RuYW1lIiwidHlwZSI6InRleHQifV0sImYiOjUsImkiOiJhY2NvdW50cyIsImwiOjk1MX0abcde12345\",\
\ \"datarows\": [[\"Abbey\"], [\"Chen\"], [\"Ani\"], [\"Peng\"], [\"John\"]]\
\}"
-- | The final continuation page: no @cursor@ because the server-side
-- context has been released automatically.
sampleFinalPage :: LBS.ByteString
sampleFinalPage =
"{\
\ \"datarows\": [[\"Eve\"], [\"Mallory\"]\
\ ]\
\}"
-- | A @POST /_plugins/_sql/close@ success response.
sampleCloseSuccess :: LBS.ByteString
sampleCloseSuccess = "{\"succeeded\":true}"
sampleCloseFailure :: LBS.ByteString
sampleCloseFailure = "{\"succeeded\":false}"
spec :: Spec
spec = describe "SQL & PPL plugin" $ do
describe "SQLColumn JSON" $ do
it "round-trips a name/type pair" $ do
let col = SQLColumn "firstname" "text"
decode (encode col) `shouldBe` Just col
it "decodes the documented schema shape" $ do
let Just (decoded :: SQLColumn) = decode "{\"name\":\"age\",\"type\":\"long\"}"
sqlColumnName decoded `shouldBe` "age"
sqlColumnType decoded `shouldBe` "long"
it "requires both fields" $ do
decode "{\"name\":\"x\"}" `shouldBe` (Nothing :: Maybe SQLColumn)
decode "{\"type\":\"x\"}" `shouldBe` (Nothing :: Maybe SQLColumn)
describe "SQLCursor JSON" $ do
it "round-trips as a bare JSON string" $ do
let cur = SQLCursor "d:abc123"
encode cur `shouldBe` "\"d:abc123\""
decode "\"d:abc123\"" `shouldBe` Just cur
it "rejects a JSON object" $ do
decode "{}" `shouldBe` (Nothing :: Maybe SQLCursor)
it "rejects a JSON array" $ do
decode "[]" `shouldBe` (Nothing :: Maybe SQLCursor)
describe "SQLParameter JSON" $ do
it "round-trips a typed bind variable" $ do
let p = SQLParameter "integer" (toJSON (30 :: Int))
decode (encode p) `shouldBe` Just p
it "decodes the documented prepared-statement shape" $ do
let Just (decoded :: SQLParameter) =
decode "{\"type\":\"integer\",\"value\":30}"
sqlParameterType decoded `shouldBe` "integer"
sqlParameterValue decoded `shouldBe` toJSON (30 :: Int)
it "requires both fields" $ do
decode "{\"type\":\"integer\"}"
`shouldBe` (Nothing :: Maybe SQLParameter)
describe "SQLCursorRequest JSON" $ do
it "encodes as a single-key {\"cursor\": ...} object" $ do
let req = SQLCursorRequest (SQLCursor "d:xyz")
encoded = LBS.toStrict (encode req)
encoded `shouldBe` "{\"cursor\":\"d:xyz\"}"
it "round-trips" $ do
let req = SQLCursorRequest (SQLCursor "d:xyz")
decode (encode req) `shouldBe` Just req
it "rejects a body without cursor" $ do
decode "{\"query\":\"SELECT 1\"}"
`shouldBe` (Nothing :: Maybe SQLCursorRequest)
describe "SQLCloseResult JSON" $ do
it "decodes the documented success body" $ do
let Just decoded = decode sampleCloseSuccess :: Maybe SQLCloseResult
sqlCloseResultSucceeded decoded `shouldBe` True
it "decodes a failure body" $ do
let Just decoded = decode sampleCloseFailure :: Maybe SQLCloseResult
sqlCloseResultSucceeded decoded `shouldBe` False
it "round-trips the success case" $ do
let Just decoded = decode sampleCloseSuccess :: Maybe SQLCloseResult
decode (encode decoded) `shouldBe` Just decoded
it "requires the succeeded field" $ do
decode "{}" `shouldBe` (Nothing :: Maybe SQLCloseResult)
describe "SQLResult JSON" $ do
it "decodes a full first-page response" $ do
let Just decoded = decode sampleFirstPage :: Maybe SQLResult
sqlResultSchema decoded
`shouldBe` Just [SQLColumn "firstname" "text", SQLColumn "lastname" "text"]
length (sqlResultDatarows decoded) `shouldBe` 2
sqlResultTotal decoded `shouldBe` Just 956
sqlResultSize decoded `shouldBe` Just 2
sqlResultStatus decoded `shouldBe` Just 200
sqlResultCursor decoded `shouldBe` Nothing
it "decodes a paginated first-page response carrying a cursor" $ do
let Just decoded = decode samplePaginatedFirstPage :: Maybe SQLResult
sqlResultCursor decoded
`shouldBe` Just
( SQLCursor
"d:eyJhIjp7fSwicyI6IkRYRjFaWEo1UVc1a1JtVjBZMmdCQUFBQUFBQUFBQU1XZWpkdFRFRkZUMlpTZEZkeFdsWnJkRlZoYnpaeVVRPT0iLCJjIjpbeyJuYW1lIjoiZmlyc3RuYW1lIiwidHlwZSI6InRleHQifV0sImYiOjUsImkiOiJhY2NvdW50cyIsImwiOjk1MX0"
)
length (sqlResultDatarows decoded) `shouldBe` 5
it "decodes a continuation page that drops schema/total/size/status" $ do
let Just decoded = decode sampleContinuationPage :: Maybe SQLResult
sqlResultSchema decoded `shouldBe` Nothing
sqlResultTotal decoded `shouldBe` Nothing
sqlResultSize decoded `shouldBe` Nothing
sqlResultStatus decoded `shouldBe` Nothing
length (sqlResultDatarows decoded) `shouldBe` 5
sqlResultCursor decoded `shouldSatisfy` isJust
it "decodes a final page with no cursor" $ do
let Just decoded = decode sampleFinalPage :: Maybe SQLResult
sqlResultCursor decoded `shouldBe` Nothing
length (sqlResultDatarows decoded) `shouldBe` 2
it "tolerates unknown fields (forward compatibility)" $ do
let Just decoded =
decode
"{\"datarows\":[],\"future_field\":42}" ::
Maybe SQLResult
sqlResultDatarows decoded `shouldBe` []
it "requires the datarows field" $ do
decode "{\"schema\":[],\"total\":0}"
`shouldBe` (Nothing :: Maybe SQLResult)
it "rejects a top-level array" $ do
decode "[]" `shouldBe` (Nothing :: Maybe SQLResult)
it "rejects malformed JSON" $ do
decode "{ not json" `shouldBe` (Nothing :: Maybe SQLResult)
it "round-trips a full first-page body through ToJSON/FromJSON" $ do
let Just decoded = decode sampleFirstPage :: Maybe SQLResult
(decode . encode) decoded `shouldBe` Just decoded
it "round-trips a continuation page through ToJSON/FromJSON" $ do
let Just decoded = decode sampleContinuationPage :: Maybe SQLResult
(decode . encode) decoded `shouldBe` Just decoded
it "ToJSON omits Nothing fields (no null leakage)" $ do
let Just decoded = decode sampleContinuationPage :: Maybe SQLResult
encoded = LBS.toStrict (encode decoded)
encoded `shouldSatisfy` not . BS.isInfixOf "\"schema\""
encoded `shouldSatisfy` not . BS.isInfixOf "\"total\""
encoded `shouldSatisfy` BS.isInfixOf "\"cursor\""
describe "SQLRequest ToJSON" $ do
it "emits only query when no optional field is set" $ do
let req = SQLRequest "SELECT * FROM accounts" Nothing Nothing Nothing
encode req `shouldBe` "{\"query\":\"SELECT * FROM accounts\"}"
it "emits filter, fetch_size, parameters when set" $ do
let filterVal = object ["term" .= object ["age" .= (30 :: Int)]]
req =
SQLRequest
{ sqlRequestQuery = "SELECT * FROM t WHERE age > ?",
sqlRequestFilter = Just (toJSON filterVal),
sqlRequestFetchSize = Just 100,
sqlRequestParameters = Just [SQLParameter "integer" (toJSON (30 :: Int))]
}
encoded = LBS.toStrict (encode req)
encoded `shouldSatisfy` BS.isInfixOf "\"query\":"
encoded `shouldSatisfy` BS.isInfixOf "\"filter\":"
encoded `shouldSatisfy` BS.isInfixOf "\"fetch_size\":100"
encoded `shouldSatisfy` BS.isInfixOf "\"parameters\":[{\"type\":\"integer\",\"value\":30}]"
it "round-trips a minimal request" $ do
let req = SQLRequest "SELECT 1" Nothing Nothing Nothing
decode (encode req) `shouldBe` Just req
it "decodes a request body that only has query" $ do
let Just (decoded :: SQLRequest) = decode "{\"query\":\"SELECT 1\"}"
sqlRequestQuery decoded `shouldBe` "SELECT 1"
sqlRequestFilter decoded `shouldBe` Nothing
sqlRequestFetchSize decoded `shouldBe` Nothing
sqlRequestParameters decoded `shouldBe` Nothing
describe "PPLRequest ToJSON" $ do
it "emits only query when no optional field is set" $ do
let req = PPLRequest "source=accounts | fields firstname" Nothing
encode req `shouldBe` "{\"query\":\"source=accounts | fields firstname\"}"
it "emits filter when set" $ do
let filterVal = object ["match_all" .= object []]
req =
PPLRequest
"source=accounts | fields firstname"
(Just (toJSON filterVal))
encoded = LBS.toStrict (encode req)
encoded `shouldSatisfy` BS.isInfixOf "\"filter\":"
it "round-trips" $ do
let req = PPLRequest "source=accounts" Nothing
decode (encode req) `shouldBe` Just req
it "decodes a request body that only has query" $ do
let Just (decoded :: PPLRequest) = decode "{\"query\":\"source=accounts\"}"
pplRequestQuery decoded `shouldBe` "source=accounts"
pplRequestFilter decoded `shouldBe` Nothing
-- =========================================================================
-- Endpoint shape (OS3). Pure checks against the BHRequest; no live backend.
-- =========================================================================
describe "sqlQuery endpoint shape (OpenSearch 3)" $ do
it "POSTs to /_plugins/_sql" $ do
let req = OS3Requests.sqlQuery (SQLRequest "SELECT 1" Nothing Nothing Nothing)
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_plugins", "_sql"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "uses the POST method" $ do
let req = OS3Requests.sqlQuery (SQLRequest "SELECT 1" Nothing Nothing Nothing)
bhRequestMethod req `shouldBe` "POST"
it "attaches the encoded SQLRequest as body" $ do
let req = OS3Requests.sqlQuery (SQLRequest "SELECT 1" Nothing Nothing Nothing)
bhRequestBody req `shouldSatisfy` isJust
it "carries the query string verbatim in the body" $ do
let req = OS3Requests.sqlQuery (SQLRequest "SELECT * FROM t" Nothing Nothing Nothing)
Just body = bhRequestBody req
LBS.toStrict body `shouldSatisfy` BS.isInfixOf "SELECT * FROM t"
describe "sqlQueryNextPage endpoint shape (OpenSearch 3)" $ do
it "POSTs to the same /_plugins/_sql path with a cursor body" $ do
let req = OS3Requests.sqlQueryNextPage (SQLCursor "d:abc")
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_plugins", "_sql"]
bhRequestMethod req `shouldBe` "POST"
let Just body = bhRequestBody req
LBS.toStrict body `shouldBe` "{\"cursor\":\"d:abc\"}"
describe "closeSqlCursor endpoint shape (OpenSearch 3)" $ do
it "POSTs to /_plugins/_sql/close with a cursor body" $ do
let req = OS3Requests.closeSqlCursor (SQLCursor "d:abc")
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_sql", "close"]
bhRequestMethod req `shouldBe` "POST"
let Just body = bhRequestBody req
LBS.toStrict body `shouldBe` "{\"cursor\":\"d:abc\"}"
describe "explainSQL endpoint shape (OpenSearch 3)" $ do
it "POSTs to /_plugins/_sql/_explain" $ do
let req = OS3Requests.explainSQL (SQLRequest "SELECT 1" Nothing Nothing Nothing)
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_plugins", "_sql", "_explain"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "uses the POST method" $ do
let req = OS3Requests.explainSQL (SQLRequest "SELECT 1" Nothing Nothing Nothing)
bhRequestMethod req `shouldBe` "POST"
it "attaches the encoded SQLRequest as body" $ do
let req = OS3Requests.explainSQL (SQLRequest "SELECT 1" Nothing Nothing Nothing)
bhRequestBody req `shouldSatisfy` isJust
it "carries the query string verbatim in the body" $ do
let req = OS3Requests.explainSQL (SQLRequest "SELECT * FROM t" Nothing Nothing Nothing)
Just body = bhRequestBody req
LBS.toStrict body `shouldSatisfy` BS.isInfixOf "SELECT * FROM t"
describe "pplQuery endpoint shape (OpenSearch 3)" $ do
it "POSTs to /_plugins/_ppl" $ do
let req = OS3Requests.pplQuery (PPLRequest "source=accounts" Nothing)
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_plugins", "_ppl"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "uses the POST method" $ do
let req = OS3Requests.pplQuery (PPLRequest "source=accounts" Nothing)
bhRequestMethod req `shouldBe` "POST"
it "attaches the encoded PPLRequest as body" $ do
let req = OS3Requests.pplQuery (PPLRequest "source=accounts" Nothing)
bhRequestBody req `shouldSatisfy` isJust
describe "explainPPL endpoint shape (OpenSearch 3)" $ do
it "POSTs to /_plugins/_ppl/_explain" $ do
let req = OS3Requests.explainPPL (PPLRequest "source=accounts" Nothing)
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_plugins", "_ppl", "_explain"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "uses the POST method" $ do
let req = OS3Requests.explainPPL (PPLRequest "source=accounts" Nothing)
bhRequestMethod req `shouldBe` "POST"
it "attaches the encoded PPLRequest as body" $ do
let req = OS3Requests.explainPPL (PPLRequest "source=accounts" Nothing)
bhRequestBody req `shouldSatisfy` isJust
it "carries the query string verbatim in the body" $ do
let req = OS3Requests.explainPPL (PPLRequest "source=accounts | fields firstname" Nothing)
Just body = bhRequestBody req
LBS.toStrict body `shouldSatisfy` BS.isInfixOf "source=accounts | fields firstname"
-- =========================================================================
-- Cross-backend parity: OS1, OS2, OS3 must agree on path/method/body.
-- =========================================================================
describe "cross-backend parity" $ do
-- Each OS version has its own SQLRequest/PPLRequest/SQLCursor types
-- (they live in distinct modules), so we build version-qualified values
-- and compare the wire output (path, method, body).
let q = "SELECT 1" :: Text
ppl = "source=accounts" :: Text
cur = "d:xyz" :: Text
it "sqlQuery emits identical paths across OS1/OS2/OS3" $ do
let p1 = getRawEndpoint (bhRequestEndpoint (OS1Requests.sqlQuery (OS1Types.SQLRequest q Nothing Nothing Nothing)))
p2 = getRawEndpoint (bhRequestEndpoint (OS2Requests.sqlQuery (OS2Types.SQLRequest q Nothing Nothing Nothing)))
p3 = getRawEndpoint (bhRequestEndpoint (OS3Requests.sqlQuery (SQLRequest q Nothing Nothing Nothing)))
p1 `shouldBe` ["_plugins", "_sql"]
p2 `shouldBe` p1
p3 `shouldBe` p1
it "sqlQuery emits identical methods across OS1/OS2/OS3" $ do
let methods =
[ bhRequestMethod (OS1Requests.sqlQuery (OS1Types.SQLRequest q Nothing Nothing Nothing)),
bhRequestMethod (OS2Requests.sqlQuery (OS2Types.SQLRequest q Nothing Nothing Nothing)),
bhRequestMethod (OS3Requests.sqlQuery (SQLRequest q Nothing Nothing Nothing))
]
-- All three should be the same single method, i.e. exactly one unique value.
length (L.nub methods) `shouldBe` 1
head methods `shouldBe` "POST"
it "sqlQuery emits identical bodies across OS1/OS2/OS3" $ do
let bodies =
catMaybes
[ bhRequestBody (OS1Requests.sqlQuery (OS1Types.SQLRequest q Nothing Nothing Nothing)),
bhRequestBody (OS2Requests.sqlQuery (OS2Types.SQLRequest q Nothing Nothing Nothing)),
bhRequestBody (OS3Requests.sqlQuery (SQLRequest q Nothing Nothing Nothing))
]
length bodies `shouldBe` 3
-- All three bodies should be byte-identical, so exactly one unique value.
length (L.nub (L.map LBS.toStrict bodies)) `shouldBe` 1
it "pplQuery emits identical paths across OS1/OS2/OS3" $ do
let p1 = getRawEndpoint (bhRequestEndpoint (OS1Requests.pplQuery (OS1Types.PPLRequest ppl Nothing)))
p2 = getRawEndpoint (bhRequestEndpoint (OS2Requests.pplQuery (OS2Types.PPLRequest ppl Nothing)))
p3 = getRawEndpoint (bhRequestEndpoint (OS3Requests.pplQuery (PPLRequest ppl Nothing)))
p1 `shouldBe` ["_plugins", "_ppl"]
p2 `shouldBe` p1
p3 `shouldBe` p1
it "closeSqlCursor emits identical paths across OS1/OS2/OS3" $ do
let p1 = getRawEndpoint (bhRequestEndpoint (OS1Requests.closeSqlCursor (OS1Types.SQLCursor cur)))
p2 = getRawEndpoint (bhRequestEndpoint (OS2Requests.closeSqlCursor (OS2Types.SQLCursor cur)))
p3 = getRawEndpoint (bhRequestEndpoint (OS3Requests.closeSqlCursor (SQLCursor cur)))
p1 `shouldBe` ["_plugins", "_sql", "close"]
p2 `shouldBe` p1
p3 `shouldBe` p1
it "sqlQueryNextPage emits identical paths across OS1/OS2/OS3" $ do
let p1 = getRawEndpoint (bhRequestEndpoint (OS1Requests.sqlQueryNextPage (OS1Types.SQLCursor cur)))
p2 = getRawEndpoint (bhRequestEndpoint (OS2Requests.sqlQueryNextPage (OS2Types.SQLCursor cur)))
p3 = getRawEndpoint (bhRequestEndpoint (OS3Requests.sqlQueryNextPage (SQLCursor cur)))
p1 `shouldBe` ["_plugins", "_sql"]
p2 `shouldBe` p1
p3 `shouldBe` p1
it "explainSQL emits identical paths across OS1/OS2/OS3" $ do
let p1 = getRawEndpoint (bhRequestEndpoint (OS1Requests.explainSQL (OS1Types.SQLRequest q Nothing Nothing Nothing)))
p2 = getRawEndpoint (bhRequestEndpoint (OS2Requests.explainSQL (OS2Types.SQLRequest q Nothing Nothing Nothing)))
p3 = getRawEndpoint (bhRequestEndpoint (OS3Requests.explainSQL (SQLRequest q Nothing Nothing Nothing)))
p1 `shouldBe` ["_plugins", "_sql", "_explain"]
p2 `shouldBe` p1
p3 `shouldBe` p1
it "explainSQL emits identical methods across OS1/OS2/OS3" $ do
let methods =
[ bhRequestMethod (OS1Requests.explainSQL (OS1Types.SQLRequest q Nothing Nothing Nothing)),
bhRequestMethod (OS2Requests.explainSQL (OS2Types.SQLRequest q Nothing Nothing Nothing)),
bhRequestMethod (OS3Requests.explainSQL (SQLRequest q Nothing Nothing Nothing))
]
length (L.nub methods) `shouldBe` 1
head methods `shouldBe` "POST"
it "explainSQL emits identical bodies across OS1/OS2/OS3" $ do
let bodies =
catMaybes
[ bhRequestBody (OS1Requests.explainSQL (OS1Types.SQLRequest q Nothing Nothing Nothing)),
bhRequestBody (OS2Requests.explainSQL (OS2Types.SQLRequest q Nothing Nothing Nothing)),
bhRequestBody (OS3Requests.explainSQL (SQLRequest q Nothing Nothing Nothing))
]
length bodies `shouldBe` 3
length (L.nub (L.map LBS.toStrict bodies)) `shouldBe` 1
it "explainPPL emits identical paths across OS1/OS2/OS3" $ do
let p1 = getRawEndpoint (bhRequestEndpoint (OS1Requests.explainPPL (OS1Types.PPLRequest ppl Nothing)))
p2 = getRawEndpoint (bhRequestEndpoint (OS2Requests.explainPPL (OS2Types.PPLRequest ppl Nothing)))
p3 = getRawEndpoint (bhRequestEndpoint (OS3Requests.explainPPL (PPLRequest ppl Nothing)))
p1 `shouldBe` ["_plugins", "_ppl", "_explain"]
p2 `shouldBe` p1
p3 `shouldBe` p1
it "explainPPL emits identical methods across OS1/OS2/OS3" $ do
let methods =
[ bhRequestMethod (OS1Requests.explainPPL (OS1Types.PPLRequest ppl Nothing)),
bhRequestMethod (OS2Requests.explainPPL (OS2Types.PPLRequest ppl Nothing)),
bhRequestMethod (OS3Requests.explainPPL (PPLRequest ppl Nothing))
]
length (L.nub methods) `shouldBe` 1
head methods `shouldBe` "POST"
it "explainPPL emits identical bodies across OS1/OS2/OS3" $ do
let bodies =
catMaybes
[ bhRequestBody (OS1Requests.explainPPL (OS1Types.PPLRequest ppl Nothing)),
bhRequestBody (OS2Requests.explainPPL (OS2Types.PPLRequest ppl Nothing)),
bhRequestBody (OS3Requests.explainPPL (PPLRequest ppl Nothing))
]
length bodies `shouldBe` 3
length (L.nub (L.map LBS.toStrict bodies)) `shouldBe` 1