packages feed

rdkafka-hs-1.1.0.0: test-integration/Kafka/Integration/ConfigStringContractTest.hs

module Kafka.Integration.ConfigStringContractTest (tests) where

import Control.Exception (bracket, throwIO, try)
import Data.Map.Strict qualified as M
import Data.Text qualified as Text
import Kafka.Consumer qualified as C
import Kafka.Dump (dumpKafkaConf)
import Kafka.Producer qualified as P
import Kafka.TestEnv
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))

tests :: TestEnv -> TestTree
tests env =
  testGroup "config-strings" $
    [ testCase ("producer global rejects " <> label) $
        rejected (P.newProducer (P.brokersList (teBrokers env) <> P.extraProp key value)) releaseProducer
    | (label, key, value) <- globalInvalid
    ]
      <> [ testCase ("producer topic rejects " <> label) $
             rejected (P.newProducer (P.brokersList (teBrokers env) <> P.extraTopicProps (M.singleton key value))) releaseProducer
         | (label, key, value) <- [("NUL key", "message.timeout.ms\0ignored", "5000"), ("NUL value", "message.timeout.ms", "5000\0ignored")]
         ]
      <> [ testCase (label <> " consumer global rejects " <> field) $
             rejected (C.newAssignedConsumer (base label mode <> C.extraProp key value)) releaseConsumer
         | (label, mode) <- modes,
           (field, key, value) <- globalInvalid
         ]
      <> [ testCase (label <> " subscription rejects " <> field) $
             rejected (C.newConsumer (base label mode) (C.extraSubscriptionProps (M.singleton key value))) releaseConsumer
         | (label, mode) <- modes,
           (field, key, value) <- [("NUL key", "auto.offset.reset\0ignored", "earliest"), ("NUL value", "auto.offset.reset", "earliest\0ignored")]
         ]
      <> [ testCase "empty producer client id retains the native default" $
             bracket (P.newProducer (P.brokersList (teBrokers env) <> P.extraProp "client.id" "") >>= either throwIO pure) releaseProducer $ \producer -> do
               config <- dumpKafkaConf producer
               M.lookup "client.id" config @?= Just "rdkafka",
           testCase "consumer ordinary UTF8 values remain exact" $
             bracket (C.newAssignedConsumer (base "ordinary" C.CallbackPollModeSync <> C.extraProp "client.id" "配置-λ") >>= either throwIO pure) releaseConsumer $ \consumer -> do
               config <- dumpKafkaConf consumer
               M.lookup "client.id" config @?= Just "配置-λ",
           testCase "empty global key retains native unknown-key rejection" $ do
             result <- try @C.KafkaError (P.newProducer (P.brokersList (teBrokers env) <> P.extraProp "" "value"))
             case result of
               Left (C.KafkaUnknownConfigurationKey _) -> pure ()
               Left failure -> assertFailure (show failure)
               Right (Left failure) -> assertFailure (show failure)
               Right (Right producer) -> releaseProducer producer >> assertFailure "empty key was accepted"
         ]
  where
    base label mode = C.brokersList (teBrokers env) <> C.groupId (makeGroupId env ("config-string-" <> Text.pack label)) <> C.callbackPollMode mode
    modes = [("async", C.CallbackPollModeAsync), ("sync", C.CallbackPollModeSync)]
    globalInvalid = [("NUL key", "client.id\0ignored", "client"), ("NUL value", "client.id", "client\0ignored")]

rejected :: IO (Either C.KafkaError a) -> (a -> IO ()) -> IO ()
rejected action release = do
  result <- try @C.KafkaError action
  case result of
    Left (C.KafkaInvalidConfigurationValue message) -> assertBool (show message) ("NUL" `Text.isInfixOf` message)
    Left failure -> assertFailure (show failure)
    Right (Left failure) -> assertFailure (show failure)
    Right (Right client) -> release client >> assertFailure "configuration silently accepted a NUL prefix"