packages feed

bloodhound-1.0.0.0: tests/Test/AutoscalingSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.AutoscalingSpec (spec) where

import Data.Aeson
import Data.Aeson.KeyMap qualified as KM
import Data.ByteString.Lazy.Char8 qualified as LBS
import Data.Maybe (fromJust, isNothing)
import TestsUtils.Import
import Prelude

------------------------------------------------------------------------------
-- Policy samples
------------------------------------------------------------------------------

-- | The PUT body from the ES 7.17 docs example: a @fixed@ decider governing
-- the @data_hot@ node role.
samplePolicyDocsBytes :: LBS.ByteString
samplePolicyDocsBytes =
  "{\
  \  \"roles\": [\"data_hot\"],\
  \  \"deciders\": {\
  \    \"fixed\": {}\
  \  }\
  \}"

-- | A policy with empty @roles@ and several deciders; both are valid wire
-- shapes the decoder must accept.
samplePolicyEmptyRolesBytes :: LBS.ByteString
samplePolicyEmptyRolesBytes =
  "{\
  \  \"roles\": [],\
  \  \"deciders\": {\
  \    \"proactive_storage\": {},\
  \    \"decider_two\": {\"some\": \"setting\"}\
  \  }\
  \}"

-- | A policy carrying an unknown sibling field (@description@) that the
-- @apExtras@ catch-all must preserve across @encode . decode@.
samplePolicyWithExtrasBytes :: LBS.ByteString
samplePolicyWithExtrasBytes =
  "{\
  \  \"roles\": [\"data_hot\", \"data_content\"],\
  \  \"deciders\": {\"fixed\": {}},\
  \  \"description\": \"tier autoscaling\"\
  \}"

------------------------------------------------------------------------------
-- Capacity samples
------------------------------------------------------------------------------

-- | @GET /_autoscaling/capacity@ with no policies configured — the empty-map
-- shape the docs use as their worked example.
sampleCapacityEmptyBytes :: LBS.ByteString
sampleCapacityEmptyBytes =
  "{\
  \  \"policies\": {}\
  \}"

-- | A full nested capacity response: required\/current capacity, two
-- current_nodes (one with an unknown sibling field), a decider result with
-- @reason_summary@ and @reason_details@, plus an unknown top-level policy
-- field — exercising every typed field and every @extras@ catch-all.
sampleCapacityFullBytes :: LBS.ByteString
sampleCapacityFullBytes =
  "{\
  \  \"policies\": {\
  \    \"my_policy\": {\
  \      \"required_capacity\": {\
  \        \"node\": {\"storage\": 1000, \"memory\": 2000},\
  \        \"total\": {\"storage\": 5000, \"memory\": 10000}\
  \      },\
  \      \"current_capacity\": {\
  \        \"node\": {\"storage\": 800, \"memory\": 1600},\
  \        \"total\": {\"storage\": 4000, \"memory\": 8000}\
  \      },\
  \      \"current_nodes\": [\
  \        {\"name\": \"node-1\"},\
  \        {\"name\": \"node-2\", \"foo\": \"bar\"}\
  \      ],\
  \      \"deciders\": {\
  \        \"proactive_storage\": {\
  \          \"required_capacity\": {\
  \            \"node\": {\"storage\": 1000},\
  \            \"total\": {\"storage\": 5000}\
  \          },\
  \          \"reason_summary\": \"need more space\",\
  \          \"reason_details\": {\"shard\": {\"index\": \"logs\", \"size\": 1234}}\
  \        }\
  \      },\
  \      \"unknown_policy_field\": 42\
  \    }\
  \  }\
  \}"

-- | A large byte count (just under 1 EiB) — guards that @storage@\/@memory@
-- parse as Int64 without overflow.
sampleLargeResourceBytes :: LBS.ByteString
sampleLargeResourceBytes =
  "{\
  \  \"storage\": 9223372036854775000,\
  \  \"memory\": 0\
  \}"

spec :: Spec
spec = describe "Autoscaling API" $ do
  describe "AutoscalingPolicyName JSON" $ do
    it "round-trips as a bare JSON string" $ do
      let Just decoded = decode "\"my-policy\"" :: Maybe AutoscalingPolicyName
      unAutoscalingPolicyName decoded `shouldBe` "my-policy"
      encode (AutoscalingPolicyName "my-policy") `shouldBe` "\"my-policy\""

    it "rejects a non-string value" $ do
      let decoded = decode "42" :: Maybe AutoscalingPolicyName
      decoded `shouldSatisfy` isNothing

  describe "AutoscalingPolicy JSON" $ do
    it "decodes the docs example into typed roles and deciders" $ do
      let Just p = decode samplePolicyDocsBytes :: Maybe AutoscalingPolicy
      apRoles p `shouldBe` ["data_hot"]
      KM.keys (apDeciders p) `shouldSatisfy` elem "fixed"
      apExtras p `shouldBe` KM.empty

    it "decodes empty roles and multiple deciders" $ do
      let Just p = decode samplePolicyEmptyRolesBytes :: Maybe AutoscalingPolicy
      apRoles p `shouldBe` []
      length (KM.toList (apDeciders p)) `shouldBe` 2

    it "preserves unknown sibling fields in apExtras" $ do
      let Just p = decode samplePolicyWithExtrasBytes :: Maybe AutoscalingPolicy
      apRoles p `shouldBe` ["data_hot", "data_content"]
      KM.lookup "description" (apExtras p) `shouldBe` Just (String "tier autoscaling")

    it "round-trips through encode . decode (extras survive)" $ do
      let Just p = decode samplePolicyWithExtrasBytes :: Maybe AutoscalingPolicy
      let Just again = decode (encode p) :: Maybe AutoscalingPolicy
      again `shouldBe` p

    it "rejects a non-object policy body" $ do
      let decoded = decode "[1,2,3]" :: Maybe AutoscalingPolicy
      decoded `shouldSatisfy` isNothing

  describe "AutoscalingCapacity JSON" $ do
    it "decodes the empty-policies response" $ do
      let Just c = decode sampleCapacityEmptyBytes :: Maybe AutoscalingCapacity
      acPolicies c `shouldBe` KM.empty

    it "decodes a full nested response into typed fields" $ do
      let Just c = decode sampleCapacityFullBytes :: Maybe AutoscalingCapacity
      acPolicies c `shouldSatisfy` (not . KM.null)
      let policy = fromJust (KM.lookup "my_policy" (acPolicies c))
      -- required_capacity.node.memory is typed and present
      let reqCap = fromJust (acpRequiredCapacity policy)
      arMemory (fromJust (capNode reqCap)) `shouldBe` Just 2000
      arStorage (fromJust (capTotal reqCap)) `shouldBe` Just 5000
      -- current_nodes: two entries, the second carrying an unknown field
      let nodes = fromJust (acpCurrentNodes policy)
      length nodes `shouldBe` 2
      acndName (nodes !! 1) `shouldBe` "node-2"
      KM.lookup "foo" (acndExtras (nodes !! 1)) `shouldBe` Just (String "bar")
      -- deciders: reason_summary typed, reason_details opaque, preserved
      let deciders = fromJust (acpDeciders policy)
      let decider = fromJust (KM.lookup "proactive_storage" deciders)
      adrReasonSummary decider `shouldBe` Just "need more space"
      let Just (Object details) = adrReasonDetails decider
      KM.size details `shouldBe` 1
      -- unknown top-level policy field survives in extras
      KM.lookup "unknown_policy_field" (acpExtras policy) `shouldBe` Just (Number 42)

    it "round-trips a full response through encode . decode" $ do
      let Just c = decode sampleCapacityFullBytes :: Maybe AutoscalingCapacity
      let Just again = decode (encode c) :: Maybe AutoscalingCapacity
      again `shouldBe` c

    it "rejects a non-object capacity body" $ do
      let decoded = decode "\"oops\"" :: Maybe AutoscalingCapacity
      decoded `shouldSatisfy` isNothing

  describe "AutoscalingResource JSON" $ do
    it "decodes large byte counts without overflow" $ do
      let Just r = decode sampleLargeResourceBytes :: Maybe AutoscalingResource
      arStorage r `shouldBe` Just 9223372036854775000
      arMemory r `shouldBe` Just 0

    it "current_nodes element rejects a missing name" $ do
      let decoded = decode "{\"foo\": \"bar\"}" :: Maybe AutoscalingCurrentNode
      decoded `shouldSatisfy` isNothing