bloodhound-1.0.0.0: tests/Test/RepositoriesMeteringSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module Test.RepositoriesMeteringSpec (spec) where
import Data.Aeson
import Data.Aeson.KeyMap qualified as KM
import Data.ByteString.Lazy.Char8 qualified as LBS
import TestsUtils.Import
import Prelude
-- | A canonical two-node response matching the documented ES shape: one
-- node with a metered S3 repository (every documented field populated)
-- and one node with an empty @repository_metering@ map.
sampleResponseBytes :: LBS.ByteString
sampleResponseBytes =
"{\
\ \"_nodes\": {\"total\": 2, \"successful\": 2, \"failed\": 0},\
\ \"cluster_name\": \"bloodhound-tests\",\
\ \"nodes\": {\
\ \"node-1\": {\
\ \"repository_metering\": {\
\ \"s3-repo:my-bucket:base/path\": {\
\ \"cloud_provider\": \"aws\",\
\ \"repo_type\": \"s3\",\
\ \"repo_endpoint\": \"s3.amazonaws.com\",\
\ \"repo_bucket\": \"my-bucket\",\
\ \"repo_base_path\": \"base/path\",\
\ \"creation_current_rate_limit\": {\"type\": \"bytes_per_second\", \"value\": 10485760},\
\ \"deletion_current_rate_limit\": {\"type\": \"bytes_per_second\", \"value\": 10485760},\
\ \"creation_total_count\": 42,\
\ \"deletion_total_count\": 7\
\ }\
\ }\
\ },\
\ \"node-2\": {\
\ \"repository_metering\": {}\
\ }\
\ }\
\}"
-- | A repository info payload carrying an unknown sibling field
-- (@future_field@) so the @rmiExtras@ round-trip catch-all is exercised.
sampleRepoInfoWithExtrasBytes :: LBS.ByteString
sampleRepoInfoWithExtrasBytes =
"{\
\ \"cloud_provider\": \"gcp\",\
\ \"repo_type\": \"gcs\",\
\ \"future_field\": true\
\}"
spec :: Spec
spec = describe "Repositories metering API" $ do
describe "RepositoriesMeteringNodesSummary JSON" $ do
it "decodes a populated _nodes summary" $ do
let Just decoded =
decode "{ \"total\": 3, \"successful\": 2, \"failed\": 1 }" ::
Maybe RepositoriesMeteringNodesSummary
rmnsTotal decoded `shouldBe` 3
rmnsSuccessful decoded `shouldBe` 2
rmnsFailed decoded `shouldBe` 1
it "defaults missing counts to 0" $ do
let Just decoded = decode "{}" :: Maybe RepositoriesMeteringNodesSummary
rmnsTotal decoded `shouldBe` 0
it "round-trips through encode . decode" $ do
let original = RepositoriesMeteringNodesSummary 1 1 0
Just roundTripped = decode (encode original)
roundTripped `shouldBe` original
describe "RepositoryMeteringInfo JSON" $ do
it "decodes the documented fields" $ do
let Just decoded =
decode sampleRepoInfoWithExtrasBytes ::
Maybe RepositoryMeteringInfo
rmiCloudProvider decoded `shouldBe` Just "gcp"
rmiRepoType decoded `shouldBe` Just "gcs"
rmiRepoEndpoint decoded `shouldBe` Nothing
rmiCreationTotalCount decoded `shouldBe` Nothing
it "preserves unknown fields in extras and round-trips" $ do
let Just decoded =
decode sampleRepoInfoWithExtrasBytes ::
Maybe RepositoryMeteringInfo
Just roundTripped =
decode (encode decoded) ::
Maybe RepositoryMeteringInfo
KM.lookup "future_field" (rmiExtras decoded) `shouldSatisfy` isJust
-- Known fields are NOT duplicated into extras.
KM.lookup "cloud_provider" (rmiExtras decoded) `shouldBe` Nothing
rmiCloudProvider roundTripped `shouldBe` Just "gcp"
KM.lookup "future_field" (rmiExtras roundTripped) `shouldSatisfy` isJust
it "decodes a fully-populated metering entry" $ do
let body =
"{\
\ \"cloud_provider\": \"azure\",\
\ \"repo_type\": \"azure\",\
\ \"repo_endpoint\": \"blob.core.windows.net\",\
\ \"repo_bucket\": \"snapshots\",\
\ \"repo_base_path\": \"repo\",\
\ \"creation_current_rate_limit\": {\"type\": \"mb_per_second\", \"value\": 10},\
\ \"deletion_current_rate_limit\": {\"type\": \"mb_per_second\", \"value\": 5},\
\ \"creation_total_count\": 100,\
\ \"deletion_total_count\": 20\
\}"
Just decoded = decode body :: Maybe RepositoryMeteringInfo
rmiCloudProvider decoded `shouldBe` Just "azure"
rmiRepoBucket decoded `shouldBe` Just "snapshots"
rmiCreationTotalCount decoded `shouldBe` Just 100
rmiDeletionTotalCount decoded `shouldBe` Just 20
rmiCreationCurrentRateLimit decoded `shouldSatisfy` isJust
it "decodes a minimal empty object without failure" $ do
let Just decoded = decode "{}" :: Maybe RepositoryMeteringInfo
rmiRepoType decoded `shouldBe` Nothing
KM.null (rmiExtras decoded) `shouldBe` True
describe "NodeRepositoriesMetering JSON" $ do
it "unwraps the repository_metering key" $ do
let Just decoded =
decode "{ \"repository_metering\": {} }" ::
Maybe NodeRepositoriesMetering
KM.null (nrmRepositories decoded) `shouldBe` True
it "defaults a missing repository_metering to empty" $ do
let Just decoded = decode "{}" :: Maybe NodeRepositoriesMetering
KM.null (nrmRepositories decoded) `shouldBe` True
describe "RepositoriesMeteringResponse JSON" $ do
it "decodes a two-node response" $ do
let Just decoded =
decode sampleResponseBytes ::
Maybe RepositoriesMeteringResponse
rmrClusterName decoded `shouldBe` Just "bloodhound-tests"
rmnsTotal <$> rmrNodesSummary decoded `shouldBe` Just 2
KM.size (rmrNodes decoded) `shouldBe` 2
let Just node1 = KM.lookup "node-1" (rmrNodes decoded)
repos = nrmRepositories node1
KM.size repos `shouldBe` 1
let Just repo = KM.lookup "s3-repo:my-bucket:base/path" repos
rmiCloudProvider repo `shouldBe` Just "aws"
rmiCreationTotalCount repo `shouldBe` Just 42
let Just node2 = KM.lookup "node-2" (rmrNodes decoded)
KM.null (nrmRepositories node2) `shouldBe` True
it "round-trips a two-node response through encode . decode" $ do
let Just decoded =
decode sampleResponseBytes ::
Maybe RepositoriesMeteringResponse
Just roundTripped =
decode (encode decoded) ::
Maybe RepositoriesMeteringResponse
rmrClusterName roundTripped `shouldBe` Just "bloodhound-tests"
KM.size (rmrNodes roundTripped) `shouldBe` 2
let Just node1 = KM.lookup "node-1" (rmrNodes roundTripped)
Just repo =
KM.lookup "s3-repo:my-bucket:base/path" (nrmRepositories node1)
rmiRepoType repo `shouldBe` Just "s3"
it "decodes a response missing _nodes and cluster_name" $ do
let Just decoded =
decode "{ \"nodes\": {} }" ::
Maybe RepositoriesMeteringResponse
rmrNodesSummary decoded `shouldBe` Nothing
rmrClusterName decoded `shouldBe` Nothing
KM.null (rmrNodes decoded) `shouldBe` True
it "encodes a minimal response, dropping Nothing fields (omitNulls)" $ do
let minimal = RepositoriesMeteringResponse Nothing Nothing mempty
encoded = encode minimal
-- @_nodes@/@cluster_name@ are 'Nothing' → dropped; @nodes@ is an
-- empty object (not 'Null') → kept.
encoded `shouldBe` "{\"nodes\":{}}"
decode encoded `shouldBe` Just minimal
describe "getRepositoriesMetering endpoint shape" $ do
let mkReq = getRepositoriesMetering
it "is a GET with no body and no query params (cluster-wide)" $ do
let req = mkReq Nothing
bhRequestMethod req `shouldBe` "GET"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_nodes", "_repositories_metering"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
bhRequestBody req `shouldBe` Nothing
it "scopes to _local for Just LocalNode" $ do
let req = mkReq (Just LocalNode)
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_nodes", "_local", "_repositories_metering"]
it "scopes to _all for Just AllNodes" $ do
let req = mkReq (Just AllNodes)
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_nodes", "_all", "_repositories_metering"]
it "scopes to a comma-joined NodeList" $ do
let req =
mkReq
( Just
(NodeList (NodeByName (NodeName "n1") :| [NodeByName (NodeName "n2")]))
)
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_nodes", "n1,n2", "_repositories_metering"]
describe "deleteRepositoriesMetering endpoint shape" $ do
it "is a DELETE with no body (cluster-wide)" $ do
let req = deleteRepositoriesMetering Nothing
bhRequestMethod req `shouldBe` "DELETE"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_nodes", "_repositories_metering"]
bhRequestBody req `shouldBe` Nothing
it "scopes to a node selection" $ do
let req = deleteRepositoriesMetering (Just AllNodes)
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_nodes", "_all", "_repositories_metering"]